I have a grid of items. Each item can expand height. I want to autoscroll when the item is expanded so it doesn't overflow the screen.
I was successful with the following code but I had to revert to a hack.
The idea was to detect when the item is overflowing using a Geometry reader on the item's background. Works wonders.
The issue is that when the view is expanded , the geo reader will update after the condition to check if autoscroll should execute is ran by the dispatcher. Hence my ugly hack.
Wonder what is the proper way ?
import SwiftUI
struct BlocksGridView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 300, maximum: .infinity), spacing: 20)]
var body: some View {
ZStack{
ScrollView {
ScrollViewReader { value in
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach((0..<20), id: \.self) {
BlockView(cardID: $0,scrollReader: value).id($0)
}
}
}
.padding(20)
}
}
}
}
struct BlockView : View {
var cardID : Int
var scrollReader : ScrollViewProxy
#State private var isOverflowingScreen = false
#State private var expand = false
var body: some View {
ZStack{
Rectangle()
.foregroundColor(isOverflowingScreen ? Color.blue : Color.green)
.frame(height: expand ? 300 : 135)
.clipShape(Rectangle()).cornerRadius(14)
.overlay(Text(cardID.description))
.background(GeometryReader { geo -> Color in
DispatchQueue.main.async {
if geo.frame(in: .global).maxY > UIScreen.main.bounds.maxY {
isOverflowingScreen = true
} else {
isOverflowingScreen = false
}
}
return Color.clear
})
.onTapGesture {
expand.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // <-- Hack :(
if isOverflowingScreen {
withAnimation{
scrollReader.scrollTo(cardID)
}
}
}
}
}
}
}
struct BlocksGridView_Previews: PreviewProvider {
static var previews: some View {
BlocksGridView()
}
}
Blue items are overflowing ...
I want to close the half sheet and back to the root view In swiftUI when with navigationLink we go to the another view. dismiss() doesn't work and turn us back to the previous view not the root view.
struct ContentView: View {
#State var showFirstSheetView = false
var body: some View {
NavigationView {
VStack {
Text("half sheet")
.onTapGesture {
showFirstSheetView.toggle()
}
}
.navigationTitle("Root view")
.sheet(isPresented: $showFirstSheetView) {
halfSheet()
}
}
}
}
struct halfSheet : View {
#State var showSecondView = false
var body: some View {
NavigationView {
VStack {
}
.background(
NavigationLink(isActive: $showSecondView, destination: {
SecondView()
}, label: {
EmptyView()
})
)
.navigationTitle("First view")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems( trailing: Button(action: {
showSecondView.toggle()
}) {
Text("Next")
}
)
}
}
}
struct SecondView : View {
#Environment(\.dismiss) var dismiss
var body: some View {
VStack {
}
.navigationTitle("Second view")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
dismiss()
}) {
HStack(spacing : 5) {
Image(systemName: "chevron.backward")
Text("Back")
}
}, trailing: Button(action: {
// How back to the root View?
// below code works but not compatible with ios 15 and gives a warning
// UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true)
}) {
Text("Done")
})
}
}
Problem:
I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title
Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?
Background:
I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.
For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.
I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.
Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.
Thanks for your help in advance! :)
Testing.swift
import SwiftUI
#main
struct Testing: App {
#StateObject var buttonsEnvironmentObject = ButtonOnOff()
var body: some Scene {
WindowGroup {
Landing().environmentObject(buttonsEnvironmentObject)
}
}
}
Landing.swift
import SwiftUI
struct Landing: View {
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
EmptyView()
}
Button(action: {
self.tag = "alpha"
}) {
HStack {
Text("alpha")
}
}
Button(action: {
self.tag = "beta"
}) {
HStack {
Text("beta")
}
}
Button(action: {
self.tag = "gamma"
}) {
HStack {
Text("gamma")
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var btnName:String
#EnvironmentObject var buttonEnvObj:ButtonOnOff
init(landingChoice:String){
self.btnName = landingChoice
print("\(self.btnName) is the input string")
}
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach(0..<buttonEnvObj.buttonNames.count) { index in
BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(buttonEnvObj.buttonNames[index]) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var label: String
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: self.label)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(label)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
.cornerRadius(15)
.overlay(buttonBrandButtons.buttonBrand[self.label]! ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
ButtonOnOff.swift
import Foundation
class ButtonOnOff:ObservableObject{
var buttonNames = ["alpha","beta","gamma"]
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
For a short answer just add
.onAppear(){
buttonEnvObj.changeState(buttonName: self.btnName)
}
to ContentView that will highlight the button that was selected.
As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.
struct Landing: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
EmptyView()
}
//Put your buttons here
HStack{
//Use the keys of the dictionary to create the buttons
ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <), id: \.self){ key in
//Have the button set the value when pressed
Button(action: {
self.tag = key
buttonEnvObj.changeState(buttonName: key)
}) {
Text(key)
}
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct ContentView: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
//Change this to use the dictionary
ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
BubbleButton(key: key, value: value)
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(value) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var key: String
var value: Bool
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: key)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(key)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(value ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(value ? Color.blue : .clear)
.cornerRadius(15)
.overlay(value ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
class ButtonOnOff:ObservableObject{
//Get rid of this so you can keep the single source
//var buttonNames = ["alpha","beta","gamma"]
//When you want to add buttons just add them here it will all adjust
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
How Can I set an item to disabled (not clickable) but visible in my tabView ?
TabView(selection: $selectedTab) {
Settings()
.tabItem {
Image(systemName: "gearshape.fill")
Text("Settings")
}.tag(1)
.disabled(true) // Not Working
I just create a way to do what you want fully supported and customisable!
test with Xcode Version 12.1, iOS 14.1, Here goes:
import SwiftUI
struct ContentView: View {
#State private var selection = 0
#State private var exSelection = 0
private var disableThis = 2
var body: some View
{
TabView(selection: $selection)
{
viewFinder(selectedIndex: selection == disableThis ? $exSelection : $selection)
.tabItem { Image(systemName: "1.circle") }
.tag(0)
viewFinder(selectedIndex: selection == disableThis ? $exSelection : $selection)
.tabItem { Image(systemName: "2.circle") }
.tag(1)
viewFinder(selectedIndex: selection == disableThis ? $exSelection : $selection)
.tabItem { Image(systemName: "3.circle") }
.tag(2)
viewFinder(selectedIndex: selection == disableThis ? $exSelection : $selection)
.tabItem { Image(systemName: "4.circle") }
.tag(3)
}
.onAppear()
{
UITabBar.appearance().barTintColor = .white
}
.accentColor(selection == disableThis ? Color.gray : Color.red)
.onChange(of: selection) { _ in
if selection != disableThis { exSelection = selection } else { selection = exSelection }
}
}
}
struct viewFinder: View
{
#Binding var selectedIndex: Int
var body: some View {
return Group
{
if selectedIndex == 0
{
FirstView()
}
else if selectedIndex == 1
{
SecondView()
}
else if selectedIndex == 2
{
ThirdView()
}
else if selectedIndex == 3
{
FourthView()
}
else
{
EmptyView()
}
}
}
}
struct FirstView: View { var body: some View {Text("FirstView")}}
struct SecondView: View { var body: some View {Text("SecondView")}}
struct ThirdView: View { var body: some View {Text("ThirdView")}}
struct FourthView: View { var body: some View {Text("FourthView")}}
There is not direct SwiftUI instrument for this now (SwiftUI 2.0), so find below possible approach based on TabBarAccessor from my another answer https://stackoverflow.com/a/59972635/12299030.
Tested with Xcode 12.1 / iOS 14.1 (note - tint color changed just for demo because disabled item is grey and invisible on grey tabbar)
struct TestTabBar: View {
init() {
UITabBar.appearance().unselectedItemTintColor = UIColor.green
}
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
Text("First View")
.background(TabBarAccessor { tabBar in
tabBar.items?.last?.isEnabled = false // << here !!
})
.tabItem { Image(systemName: "1.circle") }
.tag(0)
Text("Second View")
.tabItem { Image(systemName: "2.circle") }
.tag(1)
}
}
}
I defined 2 popovers and one sheet in the View Line().
Using this view in a VStack, everything works fine.
Using it inside a List, the wrong popovers /sheets are displayed when the corresponding text or Button is tapped.
What's going wrong here?
struct ContentView: View {
var body: some View {
VStack {
Line()
List {
Line()
Line()
Line()
}
}
}
}
struct Line: View {
#State private var showPopup1 = false
#State private var showPopup2 = false
#State private var showSheet2 = false
var body: some View {
VStack {
Text("popover 1")
.onTapGesture { self.showPopup1 = true}
.popover(isPresented: $showPopup1, arrowEdge: .trailing )
{ Popover1(showSheet: self.$showPopup1) }
.background(Color.red)
Text("popover 2")
.onTapGesture { self.showPopup2 = true }
.popover(isPresented: $showPopup2, arrowEdge: .trailing )
{ Popover2(showSheet: self.$showPopup2) }
.background(Color.yellow)
Button("Sheet2"){self.showSheet2 = true}
.sheet(isPresented: self.$showSheet2, content: { Sheet2()})
}
}
}
struct Popover1: View {
#Binding var showSheet: Bool
var body: some View {
VStack {
Text("Poppver 1 \(self.showSheet ? "T" : "F")")
Button("Cancel"){ self.showSheet = false }
}
}
}
struct Popover2: View {
#Binding var showSheet: Bool
var body: some View {
VStack {
Text("Poppver 2")
Button("Cancel"){ self.showSheet = false }
}
}
}
struct Sheet2: View {
#Environment(\.presentationMode) var presentation
var body: some View {
VStack {
Text("Sheet 2")
Button("Cancel"){ self.presentation.wrappedValue.dismiss() }
}
}
}
Just don't use Button for .sheet. List detects buttons in row and activate entire row (not sure about bug, let it be as designed). So using only and for everywhere in sub-elements gestures, makes your code work.
Tested with Xcode 11.2 / iOS 13.2
var body: some View {
VStack {
Text("popover 1")
.onTapGesture { self.showPopup1 = true}
.popover(isPresented: $showPopup1, arrowEdge: .trailing )
{ Popover1(showSheet: self.$showPopup1) }
.background(Color.red)
Text("popover 2")
.onTapGesture { self.showPopup2 = true }
.popover(isPresented: $showPopup2, arrowEdge: .trailing )
{ Popover2(showSheet: self.$showPopup2) }
.background(Color.yellow)
Text("Sheet2") // << here !!!
.onTapGesture {self.showSheet2 = true} // << here !!!
.sheet(isPresented: self.$showSheet2, content: { Sheet2()})
}
}