Back then in the dark age prior to iOS 16, performing navigation in pure SwiftUI was evil, especially for those who came from UIKit. There was only one way to navigate was to use NavigationView
+ NavigationLink
, the duo full of awful properties.
Fortunately, NavigationStack
made its first debut in iOS 16 with a huge improvement. It alleviate a lot of issues we had with NavigationView
+ NavigationLink
disastrous pair. What’s the best is we can use NavigationStack
ALONE to perform navigation in pure SwiftUI. Since NavigationStack
is better than NavigationView
in every aspect, NavigationView
is deprecated. But its friend — NavigationLink
— is not…
Even though it’s bad and we’ve already had NavigationStack
, I still see NavigationLink
from time to time to time. That’s why I write this article to tell you…
These are 3 reasons why you SHOULDN’T use NavigationLink with NavigationStack
1. Eager View Initialization
When we use NavigationLink
to link between the source and the destination views, it creates the destination view instance before it’s actually added to the view hierarchy. This leads to performance issues in real life when we construct a large number of views.
Let’s take a look at the following example, it prints to the console immediately without any interaction:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink {
ExampleView() // Destination
} label: {
Text("Tap to navigate") // Source
}
}
}
}
struct ExampleView: View {
init() {
print("init") // <--- This would be printed out before we tap the source view
}
var body: some View {
Text("Hello, World!")
}
}
The console printed out as soon as the preview finished loading:
It would be even worse if each destination view initializes it own dependencies directly (i.e. containing @StateObject
).
Solution? Use NavigationStack
with .navigationDestination
:
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button {
path.append(Destination.example)
} label: {
Text("Tap to navigate") // Source
}
.navigationDestination(for: Destination.self) { _ in
ExampleView() // Destination
}
}
}
}
enum Destination {
case example
}
After fixing, the log is printed out after I tapped the button to navigate:
This solution brings us to the next point.
2. ANTI Programmatic Navigation
Most apps need programmatic navigation. It’s a crucial part for providing seamless experience in the app. These features (but not limited to) require the ability to navigate programmatically:
- Asynchronous Navigation (i.e. go to the next screen after loading data from network)
- Deep Linking
- Handle push notifications (i.e. navigate to a certain screen after tapping a push notification)
- App Intents
Unfortunately, it’s nearly impossible to achieve that capability with NavigationLink, and don’t try it at home if your app supports minimum iOS newer than 16.
NavigationStack
and NavigationPath
support programmatic navigation by design. This is how we easily achieve deep link handling with these wonderful duo:
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
YourRootView() // <--- Source / Root
.navigationDestination(for: Destination.self) { _ in
ExampleView() // <--- Destination
}
}
// 1. Use .onOpenURL(_:) to detect incoming URL.
.onOpenURL { url in
// 2. Handle the incoming URL using a custom method.
handle(url)
}
}
private func handle(_ url: URL) {
// 3. Check if the URL is valid
// For example, we only allow appduck://example.
guard url.scheme == "appduck" else {
return
}
guard let action = components.host, action == "example" else {
return
}
// 4. Finally, perform an action corresponding to the URL.
// In this case, show ExampleView.
path.append(Destination.example)
}
}
enum Destination {
case example
}
3. No Room for Additional Actions
If you have experience working on complex apps, you know that one button often do more than one thing, such as:
- Asynchronous Operation (i.e. silently save data to the database)
- Logging
- Analytics
It’s pretty common if it’s a button made of Button
, not NavigationLink
. According to its documentation, none of its signatures accept functions:
Some developers suggest that we add additional actions to the destination view’s .onAppear()
:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink {
ExampleView() // Destination
.onAppear {
performCustomAction() // <--- BAD PRACTICE!!! Don't to this!
}
} label: {
Text("Tap to navigate") // Source
}
}
}
}
For simple apps, that trick might work. If you do that in more complex apps, you’ll end up with either a lot of duplicated code or unreadable one. This problem can be solved easily with NavigationStack
and NavigationPath
.
Conclusion
NavigationLink
is an old API designed for quick prototyping alone. Its downsides make it’s very impractical to use in professional environments.
NavigationStack
and NavigationPath
are currently the best for pure SwiftUI navigation. They are simple and quite powerful.
However, if you’re looking for a better navigation system supporting SwiftUI views with
- more performant
- more capable
- more reliable
Consider this option: UINavigationController
wrapped in a UIViewControllerRepresentable
.
Please subscribe me and my channel - AppDuck for more videos!
There are other videos I've made about cool animations in SwiftUI. I also upload Swift and SwiftUI knowledge videos weekly!
Top comments (0)