I'd like to display a TabView to display different screens. My first screen (home) displays three buttons which can select one of the three screens to display.
But(!) I have to check if a tab is repeatedly selected to trigger in this case a special action.
The detection of the repeatedly selection for screen2 works fine but then I'm not able to set a selection by the buttons.
I've tried to use an #EnvironmentalObject but changes to this object are not observed in my TabView.selection.
import SwiftUI
#main
struct TestApp: App {
static let kIndex0 = 0
static let kIndex1 = 1
static let kIndex2 = 2
var appState = AppState()
#State private var selection = TestApp.kIndex0
var body: some Scene {
// this code is required to detect a repeated selection of
// the same tab to trigger a special action
let index = Binding<Int>(
get: { self.selection },
set: {
if $0 == TestApp.kIndex1 && self.selection == $0 {
print("Trigger special action for index 1")
}
print("Pressed tab: \($0) self.selction: \(self.selection) app.selectedTab: \(appState.selectedTab)")
self.selection = $0
appState.selectedTab = $0
})
WindowGroup {
TabView(selection: index) {
First()
.environmentObject(appState)
.tabItem {
Image(systemName: "1.circle")
Text("Home")
}.tag(TestApp.kIndex0)
Text("Second Content View")
.tabItem {
Image(systemName: "2.circle")
Text("Screen Two")
}.tag(TestApp.kIndex1)
Text("Third Content View")
.tabItem {
Image(systemName: "3.circle")
Text("Screen Three")
}.tag(TestApp.kIndex2)
}
}
}
}
class AppState: ObservableObject {
#Published var selectedTab = TestApp.kIndex0
}
/*
Place to buttons which should select on of the the two
tabs of TestUI
*/
struct First: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack(alignment: .leading, spacing: 20) {
AButton(tabIndex: TestApp.kIndex0, iconName: "1.circle", text: "This first screen")
.environmentObject(appState)
AButton(tabIndex: TestApp.kIndex1, iconName: "2.circle", text: "Second screen")
.environmentObject(appState)
AButton(tabIndex: TestApp.kIndex1, iconName: "3.circle", text: "Third screen")
.environmentObject(appState)
}
}
}
struct AButton: View {
let tabIndex: Int
let iconName: String
let text: String
#EnvironmentObject var appState: AppState
var body: some View {
Button(action: {
appState.selectedTab = tabIndex
}) {
HStack() {
Image(systemName: iconName)
.imageScale(.large)
.frame(minWidth: 50)
Text(text)
}
}
}
}
You need to make appState observed and you don't need selection at all (it is just a duplicate).
I've put everything into separated ContentView (to leave scene for scene only)
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#StateObject var appState = AppState()
var body: some View {
// this code is required to detect a repeated selection of
// the same tab to trigger a special action
let index = Binding<Int>(
get: { self.appState.selectedTab },
set: {
if $0 == TestApp.kIndex1 && self.appState.selectedTab == $0 {
print("Trigger special action for index 1")
}
print("Pressed tab: \($0) app.selectedTab: \(appState.selectedTab)")
appState.selectedTab = $0
})
TabView(selection: index) {
First()
.environmentObject(appState)
.tabItem {
Image(systemName: "1.circle")
Text("Home")
}.tag(TestApp.kIndex0)
Text("Second Content View")
.tabItem {
Image(systemName: "2.circle")
Text("Screen Two")
}.tag(TestApp.kIndex1)
Text("Third Content View")
.tabItem {
Image(systemName: "3.circle")
Text("Screen Three")
}.tag(TestApp.kIndex2)
}
}
}
class AppState: ObservableObject {
#Published var selectedTab = TestApp.kIndex0
}
struct First: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack(alignment: .leading, spacing: 20) {
AButton(tabIndex: TestApp.kIndex0, iconName: "1.circle", text: "This first screen")
AButton(tabIndex: TestApp.kIndex1, iconName: "2.circle", text: "Second screen")
AButton(tabIndex: TestApp.kIndex2, iconName: "3.circle", text: "Third screen")
}
}
}
struct AButton: View {
let tabIndex: Int
let iconName: String
let text: String
#EnvironmentObject var appState: AppState
var body: some View {
Button(action: {
appState.selectedTab = tabIndex
}) {
HStack() {
Image(systemName: iconName)
.imageScale(.large)
.frame(minWidth: 50)
Text(text)
}
}
}
}
Related
I have a button ("Next Word") that when pressed shows a new view ("PracticeResult"). The idea is that everytime you press it, a new random word appears on the screen. But so far, I have to press the button twice to get it to show the next image because it's triggered with a boolean state.
I've tried changing the State back to false with an ".onAppear" toggle but it doesn't work. I've also tried using an origin State to toggle the variable back to false but it hasn't worked either. I'm quite new to SwiftUI so any tips would be appreciated! Thanks in advance.
struct PracticeView: View {
#State var isTapped: Bool = false
var body: some View {
NavigationView {
ZStack {
Color.white
VStack() {
Image("lightlogolong")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300.0, height: 100.0)
.cornerRadius(100)
.animation(.easeIn, value: 10)
NavigationLink(destination:
ContentView().navigationBarBackButtonHidden(true) {
HomeButton()
}
Group {
if (isTapped == true){
PracticeResult()
}
}.onAppear{
isTapped.toggle()
}
Button("Next Word", action:{
self.isTapped.toggle()
})
.padding()
.frame(width: 150.0, height: 40.0)
.font(.title2)
.accentColor(.black)
.background(Color("appblue"))
.clipShape(Capsule())
}
}
}
}
}
You could try this simple approach, using a UUID or something like it,
to trigger your PracticeResult view every time you tap the button. Note in particular the PracticeResult().id(isTapped)
struct PracticeView: View {
#State var isTapped: UUID? // <--- here
var body: some View {
NavigationView {
ZStack {
Color.white
VStack() {
Image("lightlogolong")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300.0, height: 100.0)
.cornerRadius(100)
.animation(.easeIn, value: 10)
NavigationLink(destination:
ContentView().navigationBarBackButtonHidden(true) {
HomeButton()
}
if isTapped != nil {
PracticeResult().id(isTapped) // <--- here
}
Button("Next Word", action:{
isTapped = UUID() // <--- here
})
.padding()
.frame(width: 150.0, height: 40.0)
.font(.title2)
.accentColor(.black)
.background(Color.blue)
.clipShape(Capsule())
}
}
}
}
}
// for testing
struct PracticeResult: View {
#State var randomWord: String = UUID().uuidString
var body: some View {
Text(randomWord)
}
}
if you want your button trigger a new random word appears on the screen, you should use button to change that random word
for example (you can try it on Preview):
struct PracticeView: View {
let words = ["one","two","three"]
#State var wordToDisplay: String?
var body: some View {
VStack {
if let wordToDisplay = wordToDisplay {
Text(wordToDisplay)
}
Spacer()
Button("Next Word") {
wordToDisplay = words.filter { $0 != wordToDisplay }.randomElement()
}
}.frame(height: 50)
}
}
I am trying to create a layout in which there is a list of elements, and a button to add more elements in front of it. I know I could put it in the navigation bar, but that's not the graphic look I'd like to achieve. However, if I put those two elements inside a ZStack, the ForEach becomes overlapped, even though it's within a VStack. How can I solve this?
import SwiftUI
struct ContentView: View {
let arrayTest = ["Element 1", "Element 2", "Element 3"]
var body: some View {
NavigationView {
ZStack {
VStack {
ForEach(arrayTest, id: \.self) { strings in
Text(strings)
}
}
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
//AddView
}) {
Image(systemName: "plus")
.background(Circle().foregroundColor(.yellow))
}.padding(.trailing, 20)
.padding(.bottom, 20)
}
}
}
}
}
}
Edit: To be more precise, I would like the button to be over the ForEach, because if I used a VStack and the list of elements was very long, the user would have to scroll all the way to the bottom to find the button. With a ZStack, it would always be visible no matter the point of the list the user is at.
Here is one way of using overlay, see example in code:
struct ContentView: View {
#State private var arrayTest: [String] = [String]()
var body: some View {
NavigationView {
Form { ForEach(arrayTest, id: \.self) { strings in Text(strings) } }
.navigationTitle("Add Elements")
}
.overlay(
Button(action: { addElement() })
{ Image(systemName: "plus").font(Font.largeTitle).background(Circle().foregroundColor(.yellow)) }.padding()
, alignment: .bottomTrailing)
.onAppear() { for _ in 0...12 { addElement() } }
}
func addElement() { arrayTest.append("Element " + "\(arrayTest.count + 1)") }
}
Don't know if I'm abusing the idea of environment object, but experiencing an issue when using an environment object that publishes a delayed async value. One view navigates to the next, but then the 'root' gets updated subsequently and as a result causes an 'echo', or even if that is handled a navigation problem. The issue becomes even more evident when using transitions between navigation.
Is there a correct use pattern to avoid this? Or some other solution maybe?
Any guidance will be appreciated.
Attached a condensed sample to illustrate the problem.
Xcode 12.4 ios 14.1
final class SetColor: ObservableObject {
#Published var asyncVal: Bool = false
func flipIt() {
DispatchQueue.main.asyncAfter(deadline: .now()+0.5, execute: {self.asyncVal.toggle()})
}
}
struct HomeView: View {
#StateObject var setCol: SetColor = SetColor()
#State private var navActive: Bool = false
var body: some View {
NavigationView {
ZStack {
Color(setCol.asyncVal ? .blue : .purple)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Home")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild1().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 1 >") })
}
}
}
}
}
}
struct NavChild1: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 1")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild2().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 2 >") })
}
}
}
}
}
struct NavChild2: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 2")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild3().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 3 >") })
}
}
}
}
}
struct NavChild3: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 3")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild3().environmentObject(setCol), isActive: .constant(false), label: { Text("Go Home") })
}
}
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
You do not need the deadline you put in GCD action. It causes navigation actions even if user does not press on navigation (I've tested the code in a project).
This is because you accumulate jobs in the GCD queue and when they are executed, you're in another View (due to the 0.5 stall). By the way, they cause navigation since the flip is Observed and therefore whoever listens , will execute the navigation.
Anyway, what you wanna do is change the dispatch command to this:
DispatchQueue.main.async { self.asyncVal.toggle() }
And navigation will be smoother with no extra navigation commands executed afterwards.
I want to be able to use my custom button in multiple places. Right now it can only Navigate to StartWithPhoneView when tapped. When I use somewhere else I want to Navigate to another view too. I can do it by creating two custom buttons but it is code repetition.
struct CustomButtonView: View {
#State var isTapped: Bool = false
var text = ""
var body: some View {
Button(action: {
print("Create account tapped")
self.isTapped.toggle()
}, label: {
RoundedRectangle(cornerRadius: 5)
.frame(width: UIScreen.main.bounds.width / 1.205, height: 44)
.foregroundColor(.blue)
.overlay(Text(text))
.foregroundColor(.white)
})
.padding(.top,15)
NavigationLink("", destination: StartWithPhoneView(), isActive: $isTapped)
}
}
I am using Custom Button in this SignUpView
struct SignupView: View {
var body: some View {
NavigationView {
VStack {
CustomButtonView(text: "Create an account" )
}
NavigationLink("", destination: StartWithPhoneView(), isActive: CustomButtonView.$isTapped) // I want to reach inside CustomButtonView to fetch isTapped
}
}
}
You can use Generic type which is a View
struct CustomButtonView<Destination: View>: View { //<-here
#State var isTapped: Bool = false
var destination: Destination //<- here
var text = ""
var body: some View {
Button(action: {
print("Create account tapped")
self.isTapped.toggle()
}, label: {
RoundedRectangle(cornerRadius: 5)
.frame(width: UIScreen.main.bounds.width / 1.205, height: 44)
.foregroundColor(.blue)
.overlay(Text(text))
.foregroundColor(.white)
})
.padding(.top,15)
NavigationLink("bbbbb", destination: destination, isActive: $isTapped) //<- here
}
}
i have a bug, if i click on button before the animation before the card flip back. i think for me the best it would be to disable the button for 2 sec, but i made some research and didnt find anything!
struct CardBack: View {
var body: some View {
Image("back_card")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
}
}
struct ContentView: View {
#State var flipped = false
#State private var cardsFront = ["bigCard1", "bigCard2", "bigCard3", "bigCard4", "bigCard5" ]
#State private var cardBack = "back_card"
#State private var disablled = true
var body: some View {
VStack {
Spacer()
ZStack {
Image(flipped ? self.cardsFront.randomElement()! : self.cardBack)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
.rotation3DEffect(Angle(degrees: flipped ? 180 : 0 ), axis: (x: 0, y: 1, z: 0))
}
Spacer()
HStack {
Button(action: {
withAnimation(.spring()) {
self.flipped.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation(.spring()) {
self.flipped.toggle()
}
}
}) {
Image("circle")
.renderingMode(.original)
}
Button(action: {
}) {
Image("plus")
.renderingMode(.original)
}
iOS 13, Swift 5
You can set the button as disabled initially and then enable it using the same sort of logic I used here.
import SwiftUI
struct ContentView: View {
#State var changeColor = false
var body: some View {
TextView(changeColor: $changeColor)
}
}
struct TextView: View {
#Binding var changeColor: Bool
var body: some View {
Text("Hello World")
.foregroundColor(changeColor ? Color.black: Color.red)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.changeColor.toggle()
}
}
}
}
You are almost there, you just need to use the .appear tag in your code to do this.