skip to Main Content

Description

For programatic navigation you could previously use NavigationLink(isActive:, destination:, label:) which would fire navigation when the isActive param is true. In IOS 16 this became deprecated and NavigationStack, NavigationLink(value:, label:) and NavigationPath was introduced.

To read about the usage of these follow the links:

https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
https://www.hackingwithswift.com/articles/250/whats-new-in-swiftui-for-ios-16 (search for NavigationStack)

My question is how should I use and maintain the array with the content of the navigation stack (like the NavigationPath object) if I’d like to use it in different Views and in their ViewModels?

As you can see in the code below I created a NavigationPath object to hold my navigation stack in the BaseView or BaseView.ViewModel. This way I can do programatic navigation from this BaseView to other pages (Page1, Page2), which is great.

But if I go to Page1 and try to navigate from there to Page2 programatically I need to have access to the original NavigationPath object object, the one that I use in BaseView.

What would be the best way to access this original object?

It is possible that I misunderstand the usage of this new feature but if you have any possible solutions for programatic navigation from a ViewModel I would be glad to see it 🙂

Code

What I’ve tried to do:

struct BaseView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        NavigationStack(path: $viewModel.paths) {
            VStack {
                Button("Page 1", action: viewModel.goToPage1)
                Button("Page 2", action: viewModel.goToPage2)
            }
            .navigationDestination(for: String.self) { stringParam in
                Page1(stringParam: stringParam)
            }
            .navigationDestination(for: Int.self) { intParam in
                Page2(intParam: intParam)
            }
            
        }
    }
}

extension BaseView {
    @MainActor class ViewModel: ObservableObject {
        @Published var paths = NavigationPath()
        
        func goToPage1() {
            let param = "Some random string" // gets the parameter from some calculation or async network call
            
            paths.append(param)
        }
        
        func goToPage2() {
            let param = 19 // gets the parameter from some calculation or async network call
            
            paths.append(param)
        }
    }
}

struct Page1: View {
    @StateObject var viewModel = ViewModel()
    let stringParam: String
    
    var body: some View {
        VStack {
            Button("Page 2", action: viewModel.goToPage2)
        }
    }
}

extension Page1 {
    @MainActor class ViewModel: ObservableObject {
        func goToPage2() {
            // Need to add value to the original paths variable in BaseView.ViewModel
        }
    }
}

struct Page2: View {
    @StateObject var viewModel = ViewModel()
    let intParam: Int
    
    var body: some View {
        Text("(intParam)")
    }
}

extension Page2 {
    @MainActor class ViewModel: ObservableObject {
    }
}

2

Answers


  1. There is no need for MVVM in SwiftUI because the View struct plus property wrappers is already equivalent to a view model object but faster and less error prone. Also in SwiftUI we don’t even have access to the traditional view layer – it takes our View data structs, diffs them to create/update/remove UIView/NSView objects, using the best ones for the platform/context. If you use an object for view data instead, then you’ll just have the same consistency problems that SwiftUI was designed to eliminate.

    Sadly the web (and Harvard University) is filled with MVVM SwiftUI articles by people that didn’t bother to learn it properly. Fortunately things are changing:

    I was wrong! MVVM is NOT a good choice for building SwiftUI applications (Azam Sharp)

    How MVVM devs get MVVM wrong in SwiftUI: From view model to state (Jim Lai)

    Stop using MVVM for SwiftUI (Apple Developer Forums)

    Login or Signup to reply.
  2. The official migration guide provides a lot of helpful information.

    The modifier navigationDestination(for:destination:) enables custom handling of specific data types.

    You can "push" chosen data types onto the NavigationPath, then the relevant navigationDestination block will handle it.

    I’ve created a few helper functions to simplify the new Navigation system.

    I store these in a custom AppContext class which you’ll see mention of below (appContext), but of course place & refer to them wherever’s best for your own codebase.

        /// The current navigation stack.
        @Published public var navStack = NavigationPath()
        
        /// Type-erased keyed data stored for a given view.
        var navData = Dictionary<String, Any>()
        
        /// Set to `true` the given "show view" bound Bool (i.e. show that view).
        /// Optionally, provide data to pass to that view.
        public func navigateTo(_ showViewFlag: Binding<Bool>,
                          _ navData: Dictionary<String, Any>? = nil) {
            if let navData { self.navData = navData }
            showViewFlag.wrappedValue = true
        }
        
        /// Pop & retrieve navigation data for the given key.
        /// (Generics undo the type-erasure produced by `navigateTo`)
        public func popNavData<T>(_ key: String) -> T {
            navData.removeValue(forKey: key)! as! T
        }
    

    This destination modifier is a tidier version of the official navigationDestination modifier:

    @ViewBuilder
    func destination(`for` show: Binding<Bool>,
                     _ destination: @escaping () -> some View ) -> some View {
        self.navigationDestination(isPresented: show) { DeferView(destination) }
    }
    

    The DeferView it uses is defined as:

    import SwiftUI
    
    public struct DeferView<Content: View>: View {
        let content: () -> Content
        public init(@ViewBuilder _ content: @escaping () -> Content) { self.content = content }
        public var body: some View { content() }
    }
    

    So now you can do this:

    // Use the standard bound-bool "show view" var format.
    @State var showMyView: Bool
    
    // Code to load the view, e.g. in a Button `action`.
    navigateTo($showMyView, ["param1": myData1, "param2", myData2])
    
    // Modifier to handle the view load, e.g. on outermost View inside `body`.
    .destination(for: $showMyView) {
        MyView(param1: appContext.popNavData("param1"),
               param2: appContext.popNavData("param2"),
               extraParam: $someOtherSource.info)
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search