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

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()
}
}
}

Related

Programmatically Presenting and Dismissing Views 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()
}
}

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 "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")
}
}

How to navigate out of a ActionSheet?

how to navigate out of a ActionSheet where I can only Pass a Text but not a NavigationLink?
Sample Code:
struct DemoActionSheetNavi: View {
#State private var showingSheet = false
var body: some View {
NavigationView {
Text("Test")
.actionSheet(isPresented: $showingSheet) {
ActionSheet(
title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [
.default(Text("How to navigate from here to HelpView???")),
])
}
}
}
}
You would need something like this:
struct DemoActionSheetNavi: View {
#State private var showingSheet = false
#State private var showingHelp = false
var body: some View {
NavigationView {
VStack {
Text("Test")
Button("Tap me") { self.showingSheet = true }
NavigationLink(destination: HelpView(isShowing: $showingHelp),
isActive: $showingHelp) {
EmptyView()
}
}
}
.actionSheet(isPresented: $showingSheet) {
ActionSheet(
title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [.cancel(),
.default(Text("Go to help")) {
self.showingSheet = false
self.showingHelp = true
}])
}
}
}
You have another state that programmatically triggers a NavigationLink (you could also do it using .sheet and modal presentation). You would also need to pass showingHelp as a #Binding to help view to be able to reset it.
struct HelpView: View {
#Binding var isShowing: Bool
var body: some View {
Text("Help view")
.onDisappear() { self.isShowing = false }
}
}

Dismiss sheet SwiftUI

I'm trying to implement a dismiss button for my modal sheet as follows:
struct TestView: View {
#Environment(\.isPresented) var present
var body: some View {
Button("return") {
self.present?.value = false
}
}
}
struct DataTest : View {
#State var showModal: Bool = false
var modal: some View {
TestView()
}
var body: some View {
Button("Present") {
self.showModal = true
}.sheet(isPresented: $showModal) {
self.modal
}
}
}
But the return button when tapped does nothing. When the modal is displayed the following appears in the console:
[WindowServer] display_timer_callback: unexpected state (now:5fbd2efe5da4 < expected:5fbd2ff58e89)
If you force unwrap present you find that it is nil
How can I dismiss .sheet programmatically?
iOS 15+
Starting from iOS 15 we can use DismissAction that can be accessed as #Environment(\.dismiss).
There's no more need to use presentationMode.wrappedValue.dismiss().
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
Use presentationMode from the #Environment.
Beta 6
struct SomeView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("Ohay!")
Button("Close") {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
For me, beta 4 broke this method - using the Environment variable isPresented - of using a dismiss button. Here's what I do nowadays:
struct ContentView: View {
#State var showingModal = false
var body: some View {
Button(action: {
self.showingModal.toggle()
}) {
Text("Show Modal")
}
.sheet(
isPresented: $showingModal,
content: { ModalPopup(showingModal: self.$showingModal) }
)
}
}
And in your modal view:
struct ModalPopup : View {
#Binding var showingModal:Bool
var body: some View {
Button(action: {
self.showingModal = false
}) {
Text("Dismiss").frame(height: 60)
}
}
}
Apple recommend (in WWDC 2020 Data Essentials in SwiftUI) using #State and #Binding for this. They also place the isEditorPresented boolean and the sheet's data in the same EditorConfig struct that is declared using #State so it can be mutated, as follows:
import SwiftUI
struct Item: Identifiable {
let id = UUID()
let title: String
}
struct EditorConfig {
var isEditorPresented = false
var title = ""
var needsSave = false
mutating func present() {
isEditorPresented = true
title = ""
needsSave = false
}
mutating func dismiss(save: Bool = false) {
isEditorPresented = false
needsSave = save
}
}
struct ContentView: View {
#State var items = [Item]()
#State private var editorConfig = EditorConfig()
var body: some View {
NavigationView {
Form {
ForEach(items) { item in
Text(item.title)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: presentEditor) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $editorConfig.isEditorPresented, onDismiss: {
if(editorConfig.needsSave) {
items.append(Item(title: editorConfig.title))
}
}) {
EditorView(editorConfig: $editorConfig)
}
}
}
func presentEditor() {
editorConfig.present()
}
}
struct EditorView: View {
#Binding var editorConfig: EditorConfig
var body: some View {
NavigationView {
Form {
TextField("Title", text:$editorConfig.title)
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(action: save) {
Text("Save")
}
.disabled(editorConfig.title.count == 0)
}
ToolbarItem(placement: .cancellationAction) {
Button(action: dismiss) {
Text("Dismiss")
}
}
}
}
}
func save() {
editorConfig.dismiss(save: true)
}
func dismiss() {
editorConfig.dismiss()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(items: [Item(title: "Banana"), Item(title: "Orange")])
}
}