skip to Main Content

I have three views A,B and C. User can navigate from A to B and from A to C. User can navigate from B to C. Now I want to differentiate if the user have come from A to C or from B to C so I was looking in how to pass extra data in NavigationStack which can help me differentiate

Below is my code

import SwiftUI

@main
struct SampleApp: App {
    
    @State private var path: NavigationPath = .init()
    
    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $path){
                A(path: $path)
                    .navigationDestination(for: ViewOptions.self) { option in
                        option.view($path)
                    }
            }
        }
    }
    
    enum ViewOptions {
        case caseB
        case caseC
        @ViewBuilder func view(_ path: Binding<NavigationPath>) -> some View{
            switch self{
            case .caseB:
                B(path: path)
            case .caseC:
                C(path: path)
            }
        }
    }
}

struct A: View {
    @Binding var path: NavigationPath
    var body: some View {
        VStack {
            Text("A")
            Button {
                path.append(SampleApp.ViewOptions.caseB)
            } label: {
                Text("Go to B")
            }
            Button {
                path.append(SampleApp.ViewOptions.caseC)
            } label: {
                Text("Go to C")
            }
        }
    }
}

struct B: View {
    @Binding var path: NavigationPath
    var body: some View {
        VStack {
            Text("B")
            Button {
                path.append(SampleApp.ViewOptions.caseC)
            } label: {
                Text("Go to C")
            }
        }
    }
}


struct C: View {
    @Binding var path: NavigationPath
    var body: some View {
        VStack {
            Text("C")
            
        }
    }
}

2

Answers


  1. Instead of "pass extra data in NavigationStack" you can pass data in a NavigationRouter. It gives you much more control

    @available(iOS 16.0, *)
    //Simplify the repetitive code
    typealias NavSource = SampleApp.ViewOptions
    @available(iOS 16.0, *)
    struct NavigationRouter{
        var path: [NavSource] = .init()
        ///Adds the provided View to the stack
        mutating func goTo(view: NavSource){
            path.append(view)
        }
        ///Searches the stack for the `View`, if the view is `nil`, the stack returns to root, if the `View` is not found the `View` is presented from the root
        mutating func bactrack(view: NavSource?){
            guard let view = view else{
                path.removeAll()
                return
            }
            //Look for the desired view
            while !path.isEmpty && path.last != view{
                path.removeLast()
            }
            //If the view wasn't found  add it to the stack
            if path.isEmpty{
                goTo(view: view)
            }
        }
        ///Identifies the previous view in the stack, returns nil if the previous view is the root
        func identifyPreviousView() -> NavSource?{
            //1 == current view, 2 == previous view
            let idx = path.count - 2
            //Make sure idx is valid index
            guard idx >= 0 else{
                return nil
            }
            //return the view
            return path[idx]
        }
    }
    

    Once you have access to the router in the Views you can adjust accordingly.

    @available(iOS 16.0, *)
    struct SampleApp: View {
        @State private var router: NavigationRouter = .init()
        var body: some View {
            NavigationStack(path: $router.path){
                A(router: $router)
                //Have the root handle the type
                .navigationDestination(for: NavSource.self) { option in
                    option.view($router)
                }
            }
        }
        //Create an `enum` so you can define your options
        //Conform to all the required protocols
        enum ViewOptions: Codable, Equatable, Hashable{
            case caseB
            case caseC
            //If you need other arguments add like this
            case unknown(String)
            //Assign each case with a `View`
            @ViewBuilder func view(_ path: Binding<NavigationRouter>) -> some View{
                switch self{
                case .caseB:
                    B(router: path)
                case .caseC:
                    C(router: path)
                case .unknown(let string):
                    Text("View for (string.description) has not been defined")
                }
            }
        }
    }
    @available(iOS 16.0, *)
    struct A: View {
        @Binding var router: NavigationRouter
        var body: some View {
            VStack{
                Button {
                    router.goTo(view: .caseB)
                } label: {
                    Text("To B")
                }
                Button {
                    router.goTo(view: .caseC)
                } label: {
                    Text("To C")
                }
            }.navigationTitle("A")
        }
    }
    @available(iOS 16.0, *)
    struct B: View {
        @Binding var router: NavigationRouter
        var body: some View {
            VStack{
                Button {
                    router.goTo(view: .caseC)
                } label: {
                    Text("Hello")
                }
                
            }.navigationTitle("B")
        }
    }
    @available(iOS 16.0, *)
    struct C: View {
        @Binding var router: NavigationRouter
        //Identify changes based on previous View
        var fromA: Bool{
            //nil is the root
            router.identifyPreviousView() == nil
        }
        var body: some View {
            VStack{
                Text("Welcome(fromA ? " Back" : "" )")
    
                Button {
                    //Append to the path the enum value
                    router.bactrack(view: router.identifyPreviousView())
                } label: {
                    Text("Back")
                }
                Button {
                    //Append to the path the enum value
                    router.goTo(view: .unknown(""some other place""))
                } label: {
                    Text("Next")
                }
                
            }.navigationTitle("C")
                .navigationBarBackButtonHidden(true)
        }
    }
    
    Login or Signup to reply.
  2. You can read the second-to-last item in the path property to learn what the previous screen was.

    To do this, it’s easier to use an actual array of ViewOptions as the path, instead of a NavigationPath.

    For example:

    struct SampleApp: App {
        // Use your own ViewOptions enum, instead of NavigationPath
        @State private var path: [ViewOptions] = []
        
        var body: some Scene {
            WindowGroup {
                NavigationStack(path: $path){
                    A(path: $path)
                        .navigationDestination(for: ViewOptions.self) { option in
                            option.view($path)
                        }
                }
            }
        }
    }
    
    struct C: View {
        @Binding var path: [ViewOptions]
    
        var previousView: ViewOptions? {
            path
                .suffix(2) // Get the last 2 elements of the path
                .first     // Get the first of those last 2 elements
        }
    
        var body: some View {
            VStack {
                Text("C")
                
            }
        }
    }
    

    Remember, a NavigationPath is nothing more than a type-erased array. It can be used to build a NavigationStack quickly without having to worry that all destination values have to match the same type. Since as you’re controlling the navigation flow with your own type ViewOptions, it makes no sense to use NavigationPath.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search