Programmatically Presenting and Dismissing Views SwiftUI - swiftui

I am working on a project that is attempting to present and dismiss views in a NavigationView using state and binding. The reason I am doing this is there is a bug in the #Environment(.presentationMode) var presentaionMode: Binding
model. It's causing odd behavior. It's discussed in this post here.
The example below has three views that are progressively loaded on to the view. The first two ContentView to NavView1 present and dismiss perfectly. However, once NavView2 is loaded, the button that is used to toggle the state of presentNavView2 ends up adding another NavView2 view on the stack and does not dismiss it as expected. Any thoughts as to why this would be?
ContentView
struct ContentView: View {
#State private var presentNavView1 = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: NavView1(presentNavView1: self.$presentNavView1), isActive: self.$presentNavView1, label: {
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("To NavView1")
}) // Button
}) // NavigationLink
} // List
.navigationTitle("Home")
} // NavigationView
} // View
}
NavView1
struct NavView1: View {
#State private var presentNavView2 = false
#Binding var presentNavView1: Bool
var body: some View {
List {
NavigationLink(destination: NavView2(presentNavView2: self.$presentNavView2), isActive: self.$presentNavView2, label: {
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("To NavView2")
}) // Button
}) // NavigationLink
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("Back")
})
} // List
.navigationTitle("NavView1")
} // View
}
NavView2
struct NavView2: View {
#Binding var presentNavView2: Bool
var body: some View {
VStack {
Text("NavView2")
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("Back")
}) // Button
} // VStack
.navigationTitle("NavView2")
}
}

You can use DismissAction, because PresentationMode will be deprecated. I tried the code and it works perfectly! Here you go!
import SwiftUI
struct MContentView: View {
#State private var presentNavView1 = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: NavView1(), isActive: self.$presentNavView1, label: {
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("To NavView1")
})
})
}
.navigationTitle("Home")
}
}
}
struct NavView1: View {
#Environment(\.dismiss) private var dismissAction: DismissAction
#State private var presentNavView2 = false
var body: some View {
List {
NavigationLink(destination: NavView2(), isActive: self.$presentNavView2, label: {
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("To NavView2")
})
})
Button(action: {
self.dismissAction.callAsFunction()
}, label: {
Text("Back")
})
}
.navigationTitle("NavView1")
}
}
struct NavView2: View {
#Environment(\.dismiss) private var dismissAction: DismissAction
var body: some View {
VStack {
Text("NavView2")
Button(action: {
self.dismissAction.callAsFunction()
}, label: {
Text("Back")
})
}
.navigationTitle("NavView2")
}
}
struct MContentView_Previews: PreviewProvider {
static var previews: some View {
MContentView()
}
}

Related

SwiftUI Half-swipe back from navigation causes error

I noticed issue in SwiftUI when using NavigationStack
Once I swipe-back on a half and revert it -> it stops working
Also I attached sample code if you want to try it
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
ListView()
}
}
}
struct ListView: View {
var body: some View {
List {
NavigationLink(destination: ViewA(viewModel: .init()), label: {
Text("A")
})
NavigationLink(destination: ViewB(), label: {
Text("B")
})
}
}
}
struct ViewA: View {
#StateObject var viewModel: Observed
var body: some View {
ZStack {
List {
Button(action: {
viewModel.action()
}, label: {
Text("label")
})
}
NavigationLink(isActive: $viewModel.shouldShowViewB, destination: {
ViewB()
}, label: {EmptyView()})
}
.navigationTitle("view a")
}
}
struct ViewB: View {
var body: some View {
List {
Button(action: {
print("actionb")
}, label: {
Text("labelb")
})
}
.navigationTitle("view b")
}
}
class Observed: ObservableObject {
#Published var shouldShowViewB = false
func action() {
print("action from model")
shouldShowViewB = true
}
}
Expected: whatever I do it should work as expected - when I tap it should open new view
Anyone else found this issue? How to fix it?
Issue 1 is you create the ObservedObject inside the NavigationLink with .init and then have a #StateObject declaration in the Subview ViewA(). That doesn't feel right. Create the Object with #StateObject in the parent view and pass it down.
Issue 2 is the new SwiftUI Navigation model, with NavigationLink)destination: label:) being deprecated. I adapted your code to the new navigation logic:
struct ContentView: View {
var body: some View {
NavigationStack {
ListView()
}
}
}
struct ListView: View {
#StateObject var viewModel = Observed() // create ObservedObject here
var body: some View {
List {
NavigationLink("A") {
ViewA(viewModel: viewModel) // pass down
}
NavigationLink("B") {
ViewB()
}
}
}
}
struct ViewA: View {
#ObservedObject var viewModel: Observed // passed down Object
var body: some View {
ZStack {
List {
Button(action: {
viewModel.action()
print(viewModel.shouldShowViewB)
}, label: {
Text("label")
})
}
.navigationDestination(isPresented: $viewModel.shouldShowViewB, destination: { ViewB() })
}
.navigationTitle("view a")
}
}
struct ViewB: View {
var body: some View {
List {
Button(action: {
print("actionb")
}, label: {
Text("labelb")
})
}
.navigationTitle("view b")
}
}
class Observed: ObservableObject {
#Published var shouldShowViewB = false
func action() {
print("action from model")
shouldShowViewB = true
}
}

In SwiftUI, iOS15, 2nd level NavigationLink, isActive is not working

in iOS15, it is not working:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1().navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}
}
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(
destination: Button {
dest2Active = false // not working!!
} label: {Text("dismiss")} .navigationTitle("Dest2"),
isActive: $dest2Active
) {Text("to Destination 2")}
}
}
The dismiss button in Dest2 is not working!
I remember that in iOS14, this code works well.
How to resolve this?
Adding .isDetailLink(false) to the top level NavigationLink seems to solve the issue. Note that this works on iPhone iOS -- for iPad, you will need to use a StackNavigationStyle as #workingdog suggests in their answer.
The documentation is not clear on why this works (in fact, it refers specifically to multi-column navigation), but it seems to solve a number of NavigationLink-related issues. See, for example: https://developer.apple.com/forums/thread/667460
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1()
.navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}.isDetailLink(false)
}
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(isActive: $dest2Active) {
Dest2(dest2Active: $dest2Active)
} label: {
Text("to Destination 2")
}
}
}
struct Dest2: View {
#Binding var dest2Active : Bool
var body: some View {
Button {
dest2Active = false
} label: {
Text("Dismiss")
}.navigationTitle("Dest2")
}
}
You need to add .navigationViewStyle(.stack) to make it work.
Here is the test code that works for me.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1().navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}
}.navigationViewStyle(.stack) // <-- here the important bit
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(
destination: Button {
dest2Active = false // now working!!
} label: {Text("dismiss")} .navigationTitle("Dest2"),
isActive: $dest2Active
) {Text("to Destination 2")}
}
}

SwiftUI NavBar button not working while timer updates view

Seems there's an issue triggering the NavBar button while timer updates the view.
In the example below, the Close button works, but the NavBar button Cancel, doesn't.
Any idea why? Or how to fix
struct ContentView: View {
#State var isPresented = false
var body: some View {
Button(action: {
isPresented = true
}, label: {
Text("Button")
})
.sheet(isPresented: $isPresented, content: {
ModalView()
})
}
}
class Model: ObservableObject {
private var monitorTimer: Timer?
#Published var someThing: Bool = false
init() {
startMonitoring()
}
func startMonitoring() {
monitorTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { _ in
self.someThing.toggle()
})
}
}
struct ModalView: View {
#Environment(\.presentationMode) private var presentationMode
#StateObject var model = Model()
var body: some View {
NavigationView {
VStack{
if model.someThing {
Text("True")
} else {
Text("False")
}
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Close")
})
}
.navigationBarTitle("Modal")
.navigationBarItems(
leading:
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
})
}
}
}
Here is tested workaround (Xcode 12 / iOS 14). The solution is to separate subviews to avoid global refresh, like below
Note: by the way - don't forget to stop timer on close modal, because state object remains alive, so now your timer runs even after sheet get closed
struct ModalView: View {
#StateObject var model = Model()
var body: some View {
InternalModal().environmentObject(model)
}
}
struct InternalModal: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
NavigationView {
VStack{
UpdatingView()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Close")
})
}
.navigationBarTitle("Modal")
.navigationBarItems(
leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
})
}
}
}
struct UpdatingView: View {
#EnvironmentObject var model: Model
var body: some View {
if model.someThing {
Text("True")
} else {
Text("False")
}
}
}

How can I check a if screen is on the navigation stack?

Given this...
NavigationLink(destination: Text("Hello")) {
Text("Press")
}
And this...
.sheet(isPresented: $viewModel.showComplete) {
Text("Hello")
}
How can I make the sheet only open if a view opened by the NavigationLink doesn't currently exist?
You may access the isActive parameter of NavigationLink and use it in a custom binding to determine whether to open the sheet.
Here is a simple demo:
struct ContentView: View {
#State var showSheet = false
#State var linkActive = false
var binding: Binding<Bool> {
.init(get: {
showSheet && !linkActive
}, set: {
showSheet = $0
})
}
var body: some View {
NavigationView {
VStack {
NavigationLink(
destination: DetailView(showSheet: $showSheet),
isActive: $linkActive
) {
Text("Go to...")
}
Button("Open sheet") {
self.showSheet.toggle()
}
}
}
.sheet(isPresented: binding) {
Text("Hello")
}
}
}
struct DetailView: View {
#Binding var showSheet: Bool
var body: some View {
Button("Open sheet") {
self.showSheet.toggle()
}
}
}

SwiftUI "Push" NavigationView/NavigationLink not working when trying to create multi-screen flow

In SwiftUI, I set up a 4 screen flow (1>2>3>4) where the user would hit "next" on each to navigate to the next screen - just like a typical push flow in UIKit. Im using "programmatic" NavigationLinks (e.g. isActive parameter) for flexibility. It gets to screen 3, but for some reason hitting next on screen 3 doesn't navigate to screen 4. Can't figure it out.
struct FlowView: View {
#State var navigateToScreen2 = false
#State var navigateToScreen3 = false
#State var navigateToScreen4 = false
var body: some View {
NavigationView {
VStack {
Text("Screen 1")
Button(action: { self.navigateToScreen2 = true }, label: { Text("Next") })
NavigationLink(destination:
VStack {
Text("Screen 2")
Button(action: { self.navigateToScreen3 = true }, label: { Text("Next") })
NavigationLink(destination:
VStack {
Text("Screen 3")
Button(action: { self.navigateToScreen4 = true}, label: { Text("Next") })
NavigationLink(destination:
Text("Screen 4"),
isActive: self.$navigateToScreen4,
label: { EmptyView() }
)
},
isActive: self.$navigateToScreen3,
label: { EmptyView() }
)
},
isActive: self.$navigateToScreen2,
label: { EmptyView() }
)
}
}
}
}
I would do it like this. It works and can be much better read:
struct ContentView: View {
#State private var navigateToScreen2 = false
var body: some View {
NavigationView {
NavigationLink(destination: View2(), isActive: $navigateToScreen2) {
Text("View1")
}
}
}
}
struct View2: View {
#State private var navigateToScreen3 = false
var body: some View {
NavigationLink(destination: View3(), isActive: $navigateToScreen3) {
Text("View2")
}
}
}
struct View3: View {
#State private var navigateToScreen4 = false
var body: some View {
NavigationLink(destination: View4(), isActive: $navigateToScreen4) {
Text("View3")
}
}
}
struct View4: View {
var body: some View {
Text("View4")
}
}