Why does withAnimation not cause an animation? - ios

I try to switch from one screen to another by pressing a button (see full code below). The switch from the first view to the second view (and vice versa) works, but no animation is taking place.
Why does this behavior happen?
Full code:
struct ContentView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
var body: some View {
if isFirstViewActive {
FirstView()
} else {
SecondView()
}
}
}
struct FirstView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
var body: some View {
ZStack {
Color(.red).ignoresSafeArea(.all, edges: .all)
VStack {
Spacer()
Text("This is the first view")
Spacer()
Button {
withAnimation {
isFirstViewActive = false
}
} label: {
Text("Go to second view")
}
Spacer()
}
}
}
}
struct SecondView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = false
var body: some View {
ZStack {
Color(.blue).ignoresSafeArea(.all, edges: .all)
VStack {
Spacer()
Text("This is the second view")
Spacer()
Button {
withAnimation {
isFirstViewActive = true
}
} label: {
Text("Go to first view")
}
Spacer()
}
}
}
}

The problem is with
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
If you change that to
#State var isFirstViewActive: Bool = true
and use #Binding in subviews, you will get the default animations.
In iOS 16, there seems to be a problem with #AppStorage vars and animation. But you can refer to this workaround

Related

SwiftUI - Remove extra space in NavigationBar after transitioning back to Home view from subview

I am new to SwiftUI and have run into a little challenge. Whenever I go from my Home view to a sub-view and then back to the Home view, I am seeing extra space created in the Navigation view (see linked GIF). I was wondering if anyone had any advice - thanks in advance!
Here is the Home Screen:
struct Home: View {
#State private var view2 = false
var body: some View {
NavigationView {
VStack {
Text("Home View!")
.padding()
NavigationLink(destination: View2(), isActive: $view2) { }
Button {
self.view2 = true
} label: {
Text("Go to next view")
}
}
.navigationTitle("Home")
}
} }
Here is the sub-new (View2):
struct View2: View {
#State private var home = false
var body: some View {
VStack {
Text("This is View 2")
.padding()
NavigationLink(destination: Home().navigationBarBackButtonHidden(true), isActive: $home) { }
Button {
self.home = true
} label: {
Text("Go to Home view")
}
}
.navigationTitle("View 1")
} }
Link to GIF:
Visual GIF of the issue
Every time you push a new Home via a NavigationLink, you're adding another NavigationView to the hierarchy, since Home has a NavigationView in it.
To avoid that, you could separate the NavigationView out and instead link to View:
struct Home: View {
var body: some View {
NavigationView {
View1() //<-- Here
}
}
}
struct View1 : View {
#State private var view2 = false
var body: some View {
VStack {
Text("Home View!")
.padding()
NavigationLink(destination: View2(), isActive: $view2) { }
Button {
self.view2 = true
} label: {
Text("Go to next view")
}
}
.navigationTitle("Home")
}
}
struct View2: View {
#State private var home = false
var body: some View {
VStack {
Text("This is View 2")
.padding()
NavigationLink(destination: View1() //<-- Here
.navigationBarBackButtonHidden(true), isActive: $home) { }
Button {
self.home = true
} label: {
Text("Go to Home view")
}
}
.navigationTitle("View 2")
}
}
That being said, I'm a little skeptical of the strategy here. It seems like instead of pushing a new View1, you might just want to be going back to the existing one. In that case, your code could just look like this:
struct Home: View {
var body: some View {
NavigationView {
View1()
}
}
}
struct View1 : View {
#State private var view2 = false
var body: some View {
VStack {
Text("Home View!")
.padding()
NavigationLink(destination: View2(), isActive: $view2) { }
Button {
self.view2 = true
} label: {
Text("Go to next view")
}
}
.navigationTitle("Home")
}
}
struct View2: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("This is View 2")
.padding()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Go to Home view")
}
}
.navigationTitle("View 2")
}
}

How can I go to a view by clicking on a Button in SwiftUI

If the user clicks on connexionBtnView I want to redirect them to an AdminView or UserView
import SwiftUI
struct ConnexionView: View {
#State var loginId: String = ""
#State var pwd: String = ""
#StateObject private var keyboardHander = KeyBoardHandler()
var body: some View {
NavigationView{
ZStack{
Image("background")
.ignoresSafeArea()
VStack (spacing: 15){
Spacer()
logoView
Spacer()
titleView
loginIdView
loginPwdView
connexionBtnView
Spacer()
NavigationLink {
LostPwdView()
} label: {
lostPwd
}
Spacer()
}.frame(maxHeight: .infinity)
.padding(.bottom,keyboardHander.keyboardHeight)
.animation(.default)
}
}
}
The NavigationLink has the isActive parameter. You can pass it in the init of NavigationLink and when this state variable has the true value you will redirect to another view. Details here.
struct ConnexionView: View {
#State var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $isActive) {
LostPwdView()
} label: {
Text("Some Label")
}
Button("Tap me!") {
isActive = true
}
}
}
}
}
struct LostPwdView: View {
var body: some View {
Text("Hello")
}
}
What you need to do is having a #State variable that would trigger the navigation:
.fullScreenCover(
isPresented: $viewShown
) {
print("View dismissed")
} content: {
NextView()
}
Where NextView() is the View you want to show and the viewShown is your State variable, below a full example:
struct ExampleView: View {
#State var isNextPageOpen = false
var body: some View {
Button("Tap Here To Navigate") {
isNextPageOpen = true
}
.fullScreenCover(
isPresented: $isNextPageOpen
) {
print("View dismissed")
} content: {
NextView()
}
}
}

Can I use a button inside a SwiftUI sheet view that appears on top of my main SwiftUI view to change a subview inside the main view?

I'm trying to use a pop up menu (that appears when the user triggers it) to make the users of my app able to change the subview that is shown inside my main view between a Subview1 and a Subview2.
I'm trying to do that using global Bool variables that are changed when a button in the view inside the sheet is pressed. Based on those values, the main view should return a different subview
The problem is that when I try to select one option from the view that appears inside the sheet, the action of the Button is performed and the sheet is dismissed but the subview displayed by the main view remains unchanged
Is there a way to change the subview or reload the main view?
The code I'm using for the main view is:
struct ContentView: View {
#State var showMenu = false
var body: some View {
if(subview1Selected){
return AnyView(SubView1())
} else if (subview2Selected){
return AnyView(SubView2())
}
else {
return AnyView(
Button(action: {
showMenu = true
})
{
Text("Button")
}
.sheet(isPresented: $showMenu, content: {
MenuView()
})
)
}
}
the code I'm using for the pop-up sheet that is used like a menu is:
struct MenuView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
List{
Button(action: {
subview1Selected = true
subview2Selected = false
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Subview1")
}
Button(action: {
subview2Selected = true
subview1Selected = false
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Subview2")
}
}
}
}
The subviews are:
struct SubView1: View {
#State var showMenu = false
var body: some View {
Button(action: {
showMenu = true
})
{
Text("SubView1")
}
.sheet(isPresented: $showMenu, content: {
MenuView()
})
}
}
and:
struct SubView2: View {
#State var showMenu = false
var body: some View {
Button(action: {
showMenu = true
})
{
Text("SubView2")
}
.sheet(isPresented: $showMenu, content: {
MenuView()
})
}
}
I think this is what you are trying to do. You can pass the #State showMenu variable as a #Binding variable into the menu. You can use a Bool if you only have 2 views, but it's more practical to use a custom enum, which I added for you. Also, the menu button should probably be separate from the subviews.
struct ContentView: View {
enum SubViewOption {
case subview1
case subview2
}
#State var showMenu = false
#State var subviewSelected: SubViewOption?
var body: some View {
ZStack() {
switch subviewSelected {
case .subview1:
SubView1()
case .subview2:
SubView2()
default:
Text("Select a view to begin.")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(
Button(action: {
showMenu.toggle()
}, label: {
Text("Menu")
.padding()
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.gray.cornerRadius(30))
.padding()
})
, alignment: .bottom
)
.sheet(isPresented: $showMenu, content: {
MenuView(subviewSelected: $subviewSelected)
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct MenuView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var subviewSelected: ContentView.SubViewOption?
var body: some View {
List {
Button(action: {
subviewSelected = .subview1
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Subview1")
})
Button(action: {
subviewSelected = .subview2
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Subview2")
})
}
}
}
struct SubView1: View {
var body: some View {
ZStack {
Color.red
.edgesIgnoringSafeArea(.all)
Text("THIS IS SUBVIEW 1")
.foregroundColor(.white)
}
}
}
struct SubView2: View {
var body: some View {
ZStack {
Color.blue
.edgesIgnoringSafeArea(.all)
Text("THIS IS SUBVIEW 2")
.foregroundColor(.white)
}
}
}

Dismissing a View using a "static nav bar"

I've created a simple view acting as a navbar which contains a menu button and some text. I'm using this as a top-level element outside my NavigationView which allows me to have the view static across all child pages that come into view. The reason I'm trying not to use the default navbar, with navbar items, is to avoid the dismissal/creation that you get along with the fading animation when you switch views.
The problem I'm now facing is dismissing the child view's when I have navigated away from the parent view. I'm able to update the button from a menu icon to a back icon, but the action of the button is not triggered. Been looking online to see if anyone has done something similar but had no luck, I'm not sure what I'm trying to achieve is even possible or whether I am going about it the right way. Is there anyway to call self.presentationMode.wrappedValue.dismiss() from the child views even though the header is initialised in the root view? Any help is appreciated, here's what I have so far:
Root View (View1):
struct View1: View {
#State var showMenuButton: Bool = false
var body: some View {
VStack {
CustomNavigationView(showMenuButton: self.showMenuButton)
NavigationView {
NavigationLink(destination: View2()) {
Text("View 2")
}
.navigationBarTitle("")
.navigationBarHidden(true)
.onDisappear(){
self.showMenuButton = false
}
.onAppear() {
self.showMenuButton = true
}
}
}
}
}
Child View of root view (View2):
struct View2: View {
var body: some View {
VStack{
Text("This is View 2")
.navigationBarTitle("")
.navigationBarHidden(true)
NavigationLink(destination: View3()) {
Text("View 3")
}
}
}
}
Child view of view 2 (View3):
struct View3: View {
var body: some View {
VStack{
Text("This is View 3")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
Custom Navigation View:
struct CustomNavigationView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var showMenuButton = false
var body: some View {
VStack {
HStack {
if showMenuButton {
Button(action: {
//Do Something
}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.black)
}
} else {
Button(action: { self.presentationMode.wrappedValue.dismiss()}) {
Image(systemName: "arrow.left")
.foregroundColor(.black)
}
}
Text("Sometext")
}
}
}
}
The environment object 'presentationMode' that you used inside the first view cannot dismiss the views you pushed. Every view that wants to be dismissed must have their own objects. The object inside the first view does not belong to any other pushed views. So, you need to create view model to manage this task.
Here is the example code. Hope that will help you solve your problem.
class NavigationObserver: ObservableObject {
private var views: [Int:Binding<PresentationMode>] = [:]
private var current: Int = 0
func popView() {
guard let view = views[current] else {
return
}
view.wrappedValue.dismiss()
views[current] = nil
current -= 1
}
func pushView(id: Int, newView: Binding<PresentationMode>) {
guard views[id] == nil else {
return
}
current += 1
views[id] = newView
}
}
struct ContentView: View {
#State var showMenuButton: Bool = false
#ObservedObject var observer = NavigationObserver()
var body: some View {
VStack {
CustomNavigationView(observer: self.observer, showMenuButton: self.showMenuButton)
NavigationView {
NavigationLink(destination: View2(observer: self.observer)) {
Text("View 2")
}
.navigationBarTitle("")
.navigationBarHidden(true)
.onDisappear(){
self.showMenuButton = false
}
.onAppear() {
self.showMenuButton = true
}
}
}
}}
struct View2: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var observer: NavigationObserver
var body: some View {
VStack{
Text("This is View 2")
.navigationBarTitle("")
.navigationBarHidden(true)
NavigationLink(destination: View3(observer: self.observer)) {
Text("View 3")
}
}.onAppear {
self.observer.pushView(id: 1, newView: self.presentationMode)
}
}}
struct View3: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var observer: NavigationObserver
var body: some View {
VStack{
Text("This is View 3")
.navigationBarTitle("")
.navigationBarHidden(true)
}.onAppear
{
self.observer.pushView(id: 2, newView: self.presentationMode)
}
}
}
struct CustomNavigationView: View {
#ObservedObject var observer: NavigationObserver
var showMenuButton = false
var body: some View {
VStack {
HStack {
if showMenuButton {
Button(action: {
//Do Something
}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.black)
}
} else {
Button(action: {
self.observer.popView()
}) {
Image(systemName: "arrow.left")
.foregroundColor(.black)
}
}
Text("Sometext")
}
}
}
}
Thanks, X_X

Presenting New Views in SwiftUI Without a Button

So I want to present a new view using SwiftUI, without the user having to tap a button, since NavigationButton would work with that. Here is an example
struct ContentView : View {
var model: Model
var body: some View {
NavigationView {
Text("Hello World")
}.onAppear {
if model.shouldPresent {
// present a new view
}
}
}
}
In the onAppear I want to include some code that will push a new view onto the navigation stack.
Here's a way to present view as a Modal.
struct PresentOnloadView: View {
var body: some View {
HStack {
Text("Hey there")
}
.presentation(Modal(HelloView(), onDismiss: nil))
}
}
struct HelloView: View {
var body: some View {
Text("Whats up! 👻")
}
}
Similarly, if you're looking to control whether to present or not using a variable, you can do something like this..
struct PresentOnloadControlledView : View {
#State var sayHello = false
var body: some View {
HStack {
Text("What's up!")
}
.onAppear(perform: {
// Decide whether to show another view or not here
self.sayHello = true
})
.presentation(sayHello ? Modal(HelloView()) : nil)
}
}
As of Version 11.0 beta 4 ➝ .presentation and Modal has been deprecated.
Not to worry! .sheet saves the day!
struct PresentOnloadControlledView : View {
#State var sayHello = false
var body: some View {
HStack {
Text("What's up!")
}
.onAppear(perform: {
// Decide whether to show another view or not here
self.sayHello = true
})
.sheet(isPresented: $sayHello) {
HelloView()
}
}
}
SwiftUI 2
Segue equivalents in SwiftUI 2 / iOS 14:
Show (Push)
struct ContentView: View {
#State var showSecondView = false
var body: some View {
NavigationView {
Text("First view")
.background(NavigationLink("", destination: Text("Second view"), isActive: $showSecondView))
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showSecondView = true
}
}
}
}
Show Detail (Replace)
struct ContentView: View {
#State private var showSecondView = false
var body: some View {
NavigationView {
if showSecondView {
Text("Second view")
} else {
Text("First view")
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showSecondView = true
}
}
}
}
Present Modally
struct ContentView: View {
#State private var showSecondView = false
var body: some View {
NavigationView {
Text("First view")
}
.sheet(isPresented: $showSecondView) {
Text("Second view")
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showSecondView = true
}
}
}
}
Present as Full Screen
struct ContentView: View {
#State private var showSecondView = false
var body: some View {
NavigationView {
Text("First view")
}
.fullScreenCover(isPresented: $showSecondView) {
Text("Second view")
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showSecondView = true
}
}
}
}
Note: I added DispatchQueue.main.asyncAfter(deadline: .now() + 1) so you can see the transition. You can remove it and the transition will start immediately.
Inside your view:
#State var present = true
var body: some View {
NavigationLink(destination: DestinationView(), isActive: $present) {
EmptyView()
}
}
This code presents DestinationView as soon as the NavigationLink loads. The crucial part is the isActive parameter, which programmatically triggers the transition to the other View.
You could rewrite as follows
#State var present = false
var body: some View {
NavigationLink(destination: DestinationView(), isActive: $present) {
Button {
present = true
} label: { Text("Present") }
}
}
To manually create a navigation button.
You can simply put the logic inside the NavigationView. Like this:
#State var x = true;
var body : some View {
NavigationView {
if x {
Text("Hello")
Button(action: {
self.x = false;
}, label: { Text("Click Me") })
} else {
Text("World")
}
}
}

Resources