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
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 ourView
data structs, diffs them to create/update/removeUIView
/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)
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 relevantnavigationDestination
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.This
destination
modifier is a tidier version of the officialnavigationDestination
modifier:The
DeferView
it uses is defined as:So now you can do this: