9

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 {
    }
}
3
  • All your views need to share the same view model. In the example you posted, each view has its own view model, so they can't share the variable paths. You shall have only one ViewModel class for all your code, and share the same instance with all views.
    – HunterLion
    Oct 31, 2022 at 20:46
  • And the object is usually called Store or ModelData and it's an environment object so doesn't need to be passed into View inits
    – malhal
    Nov 1, 2022 at 22:17
  • I actually ended up using a Singleton object as the navigation which holds the NavigationPaths variable. Nov 11, 2022 at 12:12

2 Answers 2

3

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

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)

7
  • Very interesting. Could you point me to online sources that explain this (why you should not do MVVM with SwiftUI and how SwiftUI avoided certain problems) in more detail? Dec 14, 2022 at 17:34
  • 1
    Well Swift introduced value semantics to iOS development and SwiftUI exploits it in its design. I recommend watching all the SwiftUI WWDC videos, e.g. in the following video at 4:18 he says "EditorConfig can maintain invariants on its properties and be tested independently. And because EditorConfig is a value type, any change to a property of EditorConfig, like its progress, is visible as a change to EditorConfig itself." developer.apple.com/videos/play/wwdc2020-10040 Also the second half states that ObservableObject is only used for model data not view data like MVVM is for.
    – malhal
    Dec 14, 2022 at 17:46
  • 2
    Added some links for you to check out
    – malhal
    Dec 14, 2022 at 17:55
  • Thanks Malcolm, I'll read these. I've come across the following more chaotic story: developer.apple.com/forums/thread/699003 Dec 14, 2022 at 18:59
  • This is a very misleading for new developers. The whole purpose of MVVM/MVC is encapsulation. If you're building your first todo list, sure who needs all that boilerplate code. But if you're building an application at scale, you need to be able to easily reason about the presentation versus the behavior of your application. Aug 17, 2023 at 16:16

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.