Recommended approach for migrating to modern (iOS 16+) navigation

Hello all, my team and I are looking for some advice on updating our app from using NavigationView and NavigationLink to NavigationStack and .navigationDestination now that NavigationView is deprecated.

A little background about our situation, our app is a mix of SwiftUI views and UIKit view controllers. We are slowly migrating towards primarily SwiftUI and we are at the point now where our main app entry point is SwiftUI. UIKit is now mainly just used for some legacy screens inside the app, but majority of our navigation is using SwiftUI with NavigationLinks.

I have spent a couple weeks on trying to migrate to using NavigationStack + .navigationDestination, but every time I do I keep running into issues. From what I understand, there seems to be two competing approaches for modern navigation. Those two approaches are...

  1. Having a more global navigationDestination modifier defined at the root of each tab that essentially supports navigating to all pages. I have seen this referred to as a 'router'.
  2. Applying a navigationDestination modifier on each page that navigates somewhere. This seems to be more 1-to-1 port of how we are currently using NavigationLink.

However, I tried implementing both of these solutions in our app and with both of them I ran into countless issues which made me second guess the solution I was currently implementing in favor of the other. This has led to where I am now, where I am really unsure what the recommended approach is.

I would love to hear from you all, what you have had the most success with. I am interested to hear what approach you chose, why you chose that, and then also some downsides to the approach you chose. Thanks in advance.

A good staring point would be to review the following resources:

Without being prescriptive, your app’s navigation structure would depend on your app’s structure and the various functionalities it supports, such as state restoration, deep linking etc.

@DTS Engineer

Thanks for the swift reply. I reviewed the resources you provided, and still have some remaining uncertainty around what is the recommended approach for an app like ours. Our app is much larger and complex than any of the sample apps Apple provides, and the sample apps are not complex enough to run into the same kind of challenges I have experienced. In terms of functionalities we support, we currently do not support state restoration, but we do support many deep links. Additionally, our navigation is very flexible meaning many pages can be navigated to from several different places.

I am going to give an overview of the 2 approaches I have tried so far, and the pros and cons of each. Hopefully this will be enough information that you or others could help us make an informed decision or provide solutions to our issues.

Migration Approaches

1. Navigation Destinations Defined at Root

Overview

Create a shared, reusable NavigationStack that manages…

  • Applying toolbar buttons / styling
  • Implements navigationDestination support for all destinations
  • Keeps track of which navigation path is the current one

Advantages

  • Only need to define each navigation destination → view mapping once and then it can be reused
  • Deeplink implementation is more straight forward

Disadvantages

  • Cannot pass non-hashable params to views we navigate to (view models, callbacks, etc.)
    • Option 2 has a little more flexibility with this because the view for the navigation destination is often defined where it has access to the data we want to pass
  • Less organized
    • Not as clear mental mapping of our navigation tree of possibilities
  • May need to have some complex logic that manages and keeps track of what is the current navigation stack for appending to
  • Not easy to do one-off NavigationStacks with own private view destination if any of those destinations are shared with the rest of the app

2. NavigationDestinations Applied at Each Page Which Navigates

Overview

This approach entails for each page which navigates somewhere…

  1. Define a page specific destination enum with cases for all destinations
  2. Add a navigationDestination view modifier that maps all enum cases to a view
  3. Replace all NavigationLinks with navigation APIs that use the enum case

Advantages

  • More similar to how our navigation currently works
  • Easier to feel confident that there will always be a supported navigation destination for a given push / append

Disadvantages

  • You cannot place navigationDestination view modifier inside of any lazy containers (LazyVStack, List, etc.)
    • This means that for a lot of navigation, the actual navigation is controlled from a much higher up parent than would be ideal / intuitive
  • You cannot reuse destinations. If multiple views can all navigate to the same view, they all need their own navigationDestination case.
  • You cannot have nested navigation destination modifiers of the same type
    • This really falls apart in cyclical views
      • ProfileView → FollowersList → ProfileView
      • Etc.
    • This once again leads to moving the navigation destination up to a higher parent making this more cumbersome
Recommended approach for migrating to modern (iOS 16+) navigation
 
 
Q