4

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")
            
        }
    }
}
4
  • If you expanded ViewOptions to represent all views in the path (including view A) you could use an array of [ViewOptions] instead of a NavigationPath object, and that would allow you to interoogate your hierarchy? i.e., the last item in the array is your current view, but the second-to-last will be the view before that. Commented Nov 9, 2022 at 10:29
  • Not sure I understand @ScottM Commented Nov 9, 2022 at 14:05
  • printing your path at given time should tell you the where you have come from Commented Nov 9, 2022 at 14:59
  • 1
    @Pritish NavigationPath() can represent a variety of view states being pushed onto the stack. However, if you're always pushing exactly the same type of data (e.g., always using a ViewOptions struct, you can declare @State var path: [ViewOptions] instead of using a NavigationPath object. If you do this, then you can look inside that array to see the objects that make up the current path. NavigationPath has a few collection-liek methods, but you can't look inside it in the same way as you can with arrays. Commented Nov 9, 2022 at 15:09

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.