Home > Blockchain >  How to send extra data using NavigationStack with SwiftUI?
How to send extra data using NavigationStack with SwiftUI?

Time:11-09

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

CodePudding user response:

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

CodePudding user response:

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.

  • Related