(2023) NavigationStack and NavigationDestination, using NavigationPath Correctly
Asked Answered
I

1

3

I am trying to accomplish a relatively simple task. I want to navigate from a root view -> intermediate view that captures input -> final view that displays input -> back to root view.

The solution I came up with uses the new NavigationPath and NavigationStack constructs and is partially derived from this stack overflow post: A complex Navigation with NavigationPath in SwiftUI

I created an enum with my views (NAV_ITEMS), then created a path of type NAV_ITEMS, and pushed onto that path when I wanted to go to the next view. A downfall of this solution is that I manage the state variable, called newRoomName, from the root view and pass it down as a binding to both child views, which seems wrong.

import SwiftUI

enum NAV_VIEWS {
    case AddRoomName, AddRoomSuccess, addHomeName, addHomeNameSuccess
}

struct NavPathTestView: View {
    
    @State var presentedViews = [NAV_VIEWS]()
    @State var newRoomName: String = ""

        var body: some View {
            NavigationStack(path: $presentedViews) {
                NavigationLink(value: NAV_VIEWS.AddRoomName) {
                    HStack {
                        Text("Add Room")
                    }.padding()
                        .foregroundColor(.white)
                        .background(Color.red)
                        .cornerRadius(40)
                }

                .navigationDestination(for: NAV_VIEWS.self) { i in
                    switch i {
                    case .AddRoomName:
                            RoomView(presentedViews: $presentedViews, newRoomName: $newRoomName)
                    case .AddRoomSuccess:
                            RoomSuccessView1(presentedViews: $presentedViews, newRoomName: $newRoomName)
                    case .addHomeName:
                        Text("add Home Name")
                    case .addHomeNameSuccess:
                        Text("Add Home Success")
                    }
                }
                .navigationTitle("Root Page")
            }
        }
}

struct RoomView: View {
      
    @Binding var presentedViews : [NAV_VIEWS]
    @Binding var newRoomName: String
    
    var body: some View {
        
        VStack() {
            
            Text("Add Room Name")
            Spacer()
            TextField("My new Room", text: $newRoomName)
            .padding()
            .textInputAutocapitalization(.words)
            .cornerRadius(20)
            .onSubmit {
                print("NewRoomName: \(newRoomName)")
            }
            Spacer()
            Button (action: {
                presentedViews.append(NAV_VIEWS.AddRoomSuccess)
            })
            {
                HStack {
                    Text("Go to Success")
                }.padding()
                    .foregroundColor(.white)
                    .background(Color.red)
                    .cornerRadius(40)
            }
        }
        
    }
}

struct RoomSuccessView1: View {
    
    @Binding var presentedViews : [NAV_VIEWS]
    @Binding var newRoomName: String
    
    var body: some View {
        
        
        VStack {
            Text("Room Success Page")
            Spacer()
            Text("The passed newRoomName: \(newRoomName)")
            Spacer()
            Button (action: {
                presentedViews.removeLast(presentedViews.count)
            })
            {
                HStack {
                    Text("Back To Root")
                }.padding()
                    .foregroundColor(.white)
                    .background(Color.red)
                    .cornerRadius(40)
            }
        }
    }
}


struct NavPathTestView_Previews: PreviewProvider {
    static var previews: some View {
        NavPathTestView()
    }
}

Is there a better way to do this? Routing using navigationDestination on each child view caused path issues and animation issues with how screens came into the presentation.

For example, if I used a navigationDestination on each child view (child1 -> child2) then called RootView() from the child2 view to navigate back to root, it would return, but the next time that child1 was opened it would slide in from the left hand side of the screen, different than the normal slide in from the right.

Incomplete answered 25/4, 2023 at 17:30 Comment(1)
I would change your initial design: root view > child view, where child view supports EditMode, and so can be displayed in either read-only or edit mode. That way your parent always navigates to the same view, and it's easy to navigate back. While the child has as an input whether it should activate the edit mode or not. โ€“ Eugenioeugenius
B
4

SwiftUI's NavigationStack and NavigationPath made the whole navigation much easier than before. You can use a NavigationPath like the following to handle any navigation within your whole app using only Data Type. Use navigationDestination(for: ) function to define different destination for different data types.

I faced a lot of problems first when I tried NavigationStack and NavigationPath on my own. The whole internet did not provide me a solid deep understanding about it. That's why I wrote an article on Medium with a sample project on GitHub about Multi-level nested navigation.

Read the article on Medium and run the project from GitHub

struct ContentView: View {
    @State var path: NavigationPath = .init()
    var body: some View {
        NavigationStack(path: $path.animation(.easeOut)) {
            VStack {
                Button(action: {
                    path.append(FoodItemListView.tag)
                }) {
                    MenuButton(label: "Yummy Foods ๐Ÿ•")
                }
            
                Button(action: {
                    path.append(ClothItemListView.tag)
                }) {
                    MenuButton(label: "Fashionable Cloths ๐Ÿ›๏ธ")
                }
            }.navigationTitle("SwiftUI Navigations")
            .padding(.horizontal, 20)
            .padding(.vertical, 15)
            .navigationDestination(for: String.self) { tag in
                if tag == FoodItemListView.tag {
                    FoodItemListView(path: $path)
                } else if tag == ClothItemListView.tag {
                    ClothItemListView(path: $path)
                }
            }.navigationDestination(for: FastFood.self) { foodItem in
                FoodItemDetailsView(foodItem: foodItem, path: $path)
            }.navigationDestination(for: Cloth.self) { clothItem in
                ClothItemDetailsView(clothItem: clothItem, path: $path)
            }
        }
    }
}
Bumkin answered 11/5, 2023 at 19:8 Comment(1)
You are amazing dude. I skimmed you article, struggled a bunch, then went back and read it thoroughly and it solved basically every problem I had. Some feedback, you never show in your Medium post how you define FoodItemListView.tag as a static string var to the view. I had to look in the codebase to figure it out. โ€“ Matey

© 2022 - 2024 โ€” McMap. All rights reserved.