SwiftUI Navigation backward to certain View - ios

I have a NavigationView which have multiple layers of NavigationLink.
e.g. A1->A2->A3->A4->A5.
struct A1: View {
var body: some View {
VStack{
Text("this is A1")
NavigationLink("to a2", destination: A2())
}
}
}
struct A2: View {
var body: some View {
VStack{
Text("this is A2")
NavigationLink("to a3", destination: A3())
}
}
}
struct A3: View {
var body: some View {
VStack{
Text("this is A3")
NavigationLink("to a4", destination: A4())
}
}
}
struct A4: View {
var body: some View {
VStack{
Text("this is A4")
NavigationLink("to a5", destination: A5())
}
}
}
struct A5: View {
var body: some View {
VStack{
Text("this is A5")
NavigationLink("to A2", destination: A2())
}
}
}
However, this only stack another A2() up to 6th level instead navigate back to the second level.
I've been noticed there is a #Environment(\.presentationMode) var mode: Binding<PresentationMode> and self.mode.wrappedValue.dismiss() to do the programally navigate back, but still can't find a way to do it multiple time at once.
Please help me.

You need to use the following NavigationLink in A2.
init(destination: Destination, isActive: Binding<Bool>, label: () -> Label)
Creates a navigation link that presents the destination view when active.
https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:isactive:label:)
Use the following NavigationLink in A3 and A4.
init(destination: () -> Destination, label: () -> Label)
Creates a navigation link that presents the destination view.
https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:label:)-27n7s
In A5, use a Button instad of NavigationLink.
And pass isActive upto A5. Toggle isActive when the button is clicked.
In this example, A2 is the view that you want to return to.
struct A2: View {
#State private var isActive: Bool = false
var body: some View {
NavigationView {
NavigationLink(destination: A3(isActive: self.$isActive), isActive: $isActive) {TText("this is A2") }
}
}
}
struct A3: View {
#Binding var isActive: Bool
var body: some View {
VStack {
NavigationLink(destination: A4(isActive: self.$isActive)) {Text("this is A3"))}
}
}
}
struct A4: View {
#Binding var isActive: Bool
var body: some View {
VStack {
NavigationLink(destination: A5(isActive: self.$isActive)) {Text("this is A4"))}
}
}
}
struct A5: View {
#Binding var isActive: Bool
var body: some View {
Button(action: {
self.isActive.toggle()
}, label: {
Text("this is A5")
})
}
}

Related

SwiftUI - How can I dismiss a view to root and then push a second view immediately afterwards?

Couldn't find anything relating to this issue in SwiftUI.
I have three views currently, RootView, DetailView1 and DetailView2. RootView will feature a button to push and show DetailView1, inside DetailView1 there will be a NavigationLink to dismiss DetailView1 to RootView and push DetailView2.
struct DetailView1: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
NavigationLink(destination: DetailView2()) {
Text("Tap to dismiss DetailView and show DetailView2")
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
struct DetailView2: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(
"This is DetailView2",
action: { self.presentationMode.wrappedValue.dismiss() }
)
}
}
struct RootView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView1())
{ Text("This is root view. Tap to go to DetailView") }
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
RootView()
}
}
}
Expected behaviour:
User presses NavigationLink in DetailView1, the view is dismissed to RootView and DetailView2 is pushed up.
Current behaviour:
User presses NavigationLink in DetailView1, the view is dismissed to RootView, DetailView2 is not pushed.
You can do this by passing the active state of the second view. And change the #State value. It's a similar concept to the completion block in UIKit.
DetailView1
struct DetailView1: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Binding var isActiveDetails2: Bool //<-- Here
var body: some View {
VStack {
Button("Tap to dismiss DetailView and show DetailView2") {
isActiveDetails2 = true //<-- Here
}
}
}
}
RootView
struct RootView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var isActiveDetails2: Bool = false
var body: some View {
VStack {
NavigationLink(destination: DetailView1(isActiveDetails2: $isActiveDetails2))
{ Text("This is root view. Tap to go to DetailView") }
NavigationLink(destination: DetailView2(), isActive: $isActiveDetails2) { //<-- Here
EmptyView()
}
}
}
}

Last view is instantly popped after programaticaly pushing 2 views SwiftUI

I'm trying to push views programmatically in SwiftUI, but when I push two views using the isActive property of NavigationLink the last view is pushed and instantly popped.
I'm not sure if I'm doing something wrong or if this is a bug with SwuiftUI.
This is how it looks when I try to push the two views:
If I push one view and then the other some times it works:
This is my code. I'm passing appState as an EnvironmentObject
class AppState: ObservableObject {
#Published var showView1 = false
#Published var showView2 = false
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
Button(action: {
appState.showView2 = false
appState.showView1 = true
}) {
Text("Show view 1")
}
Button(action: {
appState.showView2 = true
appState.showView1 = true
}) {
Text("Show view 2")
}
NavigationLink( destination: View1(), isActive: $appState.showView1, label: { EmptyView() })
}
}
}
}
struct View1: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack {
Text("This is view1")
Button(action: {
appState.showView2 = true
}) {
Text("Show view2")
}
NavigationLink( destination: View2(), isActive: $appState.showView2, label: { EmptyView() })
}
}
}
struct View2: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack {
Text("This is view2")
Button(action: {
appState.showView1 = false
appState.showView2 = false
}) {
Text("Back to root")
}
}
}
}
This may be related to the bug described here https://stackoverflow.com/a/57274613/2584078
But in this case is the opposite. The view is popped insted of pushed
I'm using Xcode 12.0 beta 2 (12A6163b)

NavigationLink repeats in swiftUi

struct Conte111ntView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View : click go to ThirdView ") .navigationBarTitle("Navigation").navigationBarHidden(true).gesture(TapGesture().onEnded{ v in
self.selection = "Third"
}), tag: "Second", selection: $selection) { EmptyView() }.isDetailLink(true)
NavigationLink(destination: Text("Third View : click go to SecondView ") .navigationBarTitle("Navigation").navigationBarHidden(true).gesture(TapGesture().onEnded{ v in
self.selection = "Second"
}), tag: "Third", selection: $selection) { EmptyView() }.isDetailLink(true)
Button("Tap to show second") {
self.selection = "Second"
}
Button("Tap to show third") {
self.selection = "Third"
}
}
.navigationBarTitle("Navigation").navigationBarHidden(true)
}
}
}
struct test_Previews: PreviewProvider {
static var previews: some View {
Conte111ntView()
}
}
I want to Second View -> Third View
but swiftUi behavior is: Second View -> rootView -> Third View
And quick tap in 'click go to ThirdView' And ,'Third View'
it get the wrong behavior 。 return to rootView
how can fix this
Or am I doing it the wrong way?
The following is a simpler version.
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SecondView()) {
Text("Second View : click go to ThirdView")
}
Spacer()
NavigationLink(destination: ThirdView()) {
Text("Third View : click go to SecondView")
}
}
}
.navigationBarHidden(false)
}
}
struct SecondView: View {
var body: some View {
Text("SecondView is here!")
}
}
struct ThirdView: View {
var body: some View {
Text("ThirdView is here!")
}
}

SwiftUI transition from modal sheet to regular view with Navigation Link

I'm working with SwiftUI and I have a starting page. When a user presses a button on this page, a modal sheet pops up.
In side the modal sheet, I have some code like this:
NavigationLink(destination: NextView(), tag: 2, selection: $tag) {
EmptyView()
}
and my modal sheet view is wrapped inside of a Navigation View.
When the value of tag becomes 2, the view does indeed go to NextView(), but it's also presented as a modal sheet that the user can swipe down from, and I don't want this.
I'd like to transition from a modal sheet to a regular view.
Is this possible? I've tried hiding the navigation bar, etc. but it doesn't seem to make a difference.
Any help with this matter would be appreciated.
You can do this by creating an environmentObject and bind the navigationLink destination value to the environmentObject's value then change the value of the environmentObject in the modal view.
Here is a code explaining what I mean
import SwiftUI
class NavigationManager: ObservableObject{
#Published private(set) var dest: AnyView? = nil
#Published var isActive: Bool = false
func move(to: AnyView) {
self.dest = to
self.isActive = true
}
}
struct StackOverflow6: View {
#State var showModal: Bool = false
#EnvironmentObject var navigationManager: NavigationManager
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: self.navigationManager.dest, isActive: self.$navigationManager.isActive) {
EmptyView()
}
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}
}
}
.sheet(isPresented: self.$showModal) {
secondView(isPresented: self.$showModal).environmentObject(self.navigationManager)
}
}
}
struct StackOverflow6_Previews: PreviewProvider {
static var previews: some View {
StackOverflow6().environmentObject(NavigationManager())
}
}
struct secondView: View {
#EnvironmentObject var navigationManager: NavigationManager
#Binding var isPresented: Bool
#State var dest: AnyView? = nil
var body: some View {
VStack {
Text("Modal view")
Button(action: {
self.isPresented = false
self.dest = AnyView(thirdView())
}) {
Text("Press me to navigate")
}
}
.onDisappear {
// This code can run any where but I placed it in `.onDisappear` so you can see the animation
if let dest = self.dest {
self.navigationManager.move(to: dest)
}
}
}
}
struct thirdView: View {
var body: some View {
Text("3rd")
.navigationBarTitle(Text("3rd View"))
}
}
Hope this helps, if you have any questions regarding this code, please let me know.

Dismiss a parent modal in SwiftUI from a NavigationView

I am aware of how to dismiss a modal from a child view using #Environment (\.presentationMode) var presentationMode / self.presentationMode.wrappedValue.dismiss() but this is a different issue.
When you present a multi-page NavigationView in a modal window, and have navigated through a couple of pages, the reference to presentationMode changes to be the NavigationView, so using self.presentationMode.wrappedValue.dismiss() simply pops the last NavigationView rather than dismissing the containing modal.
Is it possible - and if so how - to dismiss the containing modal from a page in a NavigationView tree?
Here's a simple example showing the problem. If you create an Xcode Single View app project using SwiftUI and replace the default ContentView code with this, it should work with no further changes.
import SwiftUI
struct ContentView: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
self.showModal = false
}) {
PageOneContent()
}
}
}
struct PageOneContent: View {
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
trailing: NavigationLink(destination: PageTwoContent()) {
Text("Next")
})
}
}
}
struct PageTwoContent: View {
#Environment (\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
// How to dismiss parent modal here instead
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
Here is possible approach based on usage own explicitly created environment key (actually I have feeling that it is not correct to use presentationMode for this use-case.. anyway).
Proposed approach is generic and works from any view in modal view hierarchy. Tested & works with Xcode 11.2 / iOS 13.2.
// define env key to store our modal mode values
struct ModalModeKey: EnvironmentKey {
static let defaultValue = Binding<Bool>.constant(false) // < required
}
// define modalMode value
extension EnvironmentValues {
var modalMode: Binding<Bool> {
get {
return self[ModalModeKey.self]
}
set {
self[ModalModeKey.self] = newValue
}
}
}
struct ParentModalTest: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
}) {
PageOneContent()
.environment(\.modalMode, self.$showModal) // < bind modalMode
}
}
}
struct PageOneContent: View {
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
trailing: NavigationLink(destination: PageTwoContent()) {
Text("Next")
})
}
}
}
struct PageTwoContent: View {
#Environment (\.modalMode) var modalMode // << extract modalMode
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
self.modalMode.wrappedValue = false // << close modal
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
Another Approach would be to simply use a notification for this case and just reset the triggering flag for your modal.
It is not the most beautiful solution for me but it is the solution I am most likely to still understand in a few months.
import SwiftUI
struct ContentView: View {
#State var showModalNav: Bool = false
var body: some View {
Text("Present Modal")
.padding()
.onTapGesture {
showModalNav.toggle()
}.sheet(isPresented: $showModalNav, content: {
ModalNavView()
}).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
showModalNav = false
}
}
}
struct ModalNavView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: PushedView(),
label: {
Text("Show Another View")
}
)
}
}
}
struct PushedView: View {
var body: some View {
Text("Pushed View").onTapGesture {
NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "PushedViewNotifciation")))
}
}
}
If you don't want to loosely couple the views through a notification you could also just use a binding for this like so:
struct ContentView: View {
#State var showModalNav: Bool = false
var body: some View {
Text("Present Modal")
.padding()
.onTapGesture {
showModalNav.toggle()
}.sheet(isPresented: $showModalNav, content: {
ModalNavView(parentShowModal: $showModalNav)
}).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
showModalNav = false
}
}
}
struct ModalNavView: View {
#Binding var parentShowModal: Bool
var body: some View {
NavigationView {
NavigationLink(
destination: PushedView(parentShowModal: $parentShowModal),
label: {
Text("Show Another View")
}
)
}
}
}
struct PushedView: View {
#Binding var parentShowModal: Bool
var body: some View {
Text("Pushed View").onTapGesture {
parentShowModal = false
}
}
}
If it's only two levels, and especially if you can dismiss the sheet at multiple levels, you could include showModal as a binding variable in your navigation views below, and then toggling it anywhere would dismiss the entire sheet.
I would assume you could do something similar with showModal as an EnvironmentObject as Wei mentioned above - which might be better if there are more than two levels and you only want to dismiss the sheet at the most specific level.
I can't remember if there's some reason to move away from doing this as a binding variable, but it seems to be working for me.
import SwiftUI
struct ContentView: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
self.showModal = false
}) {
// Bind showModal to the corresponding property in PageOneContent
PageOneContent(showModal: $showModal)
}
}
}
Then you add showModal as a binding variable in PageOneContent, and it is bound to the state variable in ContentView.
struct PageOneContent: View {
// add a binding showModal var in here
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
// bind showModal again to PageTwoContent
trailing: NavigationLink(destination: PageTwoContent(showModal: $showModal)) {
Text("Next")
})
}
}
}
Finally, in PageTwoContent, you can add showModal here (and in the NavigationLink in PageOneContent, you have bound PageTwoContent's showModal to PageOneContent). Then in your button, all you have to do is toggle it, and it will dismiss the sheet.
struct PageTwoContent: View {
// Add showModal as a binding var here too.
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
// This will set the showModal var back to false in all three views, and will dismiss the current sheet.
self.showModal.toggle()
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
I found out you can actually make showModal into an EnvironmentObject, then simplify toggle the showModal to false on PageTwoContent to dismiss both PageOneContent and PageTwoContent.

Resources