When writing an application using AppKit or UIKit you feel can confident as to when views are created and released. Your code takes explicit actions for that to happen.
SwiftUI is a big more nebulous. Let's look at a typical example. Below is a simple SwiftUI application that shows a partial list of SFSymbols and allows you to tap on one to see a larger version.
If you're familiar with AppKit and UIKit, you'd expect that the detail view would be created when you tap on a row in the symbol list.
But when dealing with SwiftUI, that's not the case. In fact, there are some cases where every view that a NavigationLink references is created even before the row is tapped on.
We're going to examine three different means of creating this same interface, and see how the small changes we make affect the execution. You may be surprised by some of the results.
The Underlying Models
First, lets over over the underlying models as they are identical amongst the implementations demonstrated here.
The model object is the SFSymbol.swift
class, shown below.
This provides the model content to the HostView.swift
file as well as the SymbolVideo.swift
.
The HostViewModel.swift
is also quite straightforward. It iterates over an array of SFSymbol names and generates SFSymbol
models for each.
Cases to Examine
There really three cases we'll examine:
- using
NavigationLink(_:destination)
and the unexpected pitfalls, - using
NavigationLink(_:destination)
and a means of avoiding those pitfalls, - using
NavigationLink(value:)
which was added in iOS 16 that eliminates these issues entirely.
Case 1: NavigationLink(_:destination)
If you're using NavigationLink inside of a List and a ForEach, you just might be surprised at the behaviour.
There is the HomeView.swift file for case 1
This is a very simple list that loops through ForEach of the symbols and creates a NavigationLink with a label and designation for each one.
However, if insert a print
in the init
statement of the SymbolView.swift
file, you'll be surprised to see this:
Before you even tap on a row, every one of the SymbolViews have already ready been initialized, twice! If you were doing anything computationally heavy in your initial method, it would drastically slow down the process.
So, how can we work around this?
Case 2: NavigationLink(_:destination)
and the .onAppear
Modifier.
The .onAppear
modifier is called when a view is first displayed. This allows you to delay creation of your view modifier and any complex work it may need to do, till it is required.
The only change required to adopt .onAppear
is in the SymbolView.swift file.
Adding .onAppear
requires no change to the HomeView.swift
class. And if you're unable to target iOS 16 as a minimum, it's a great solution to the problem. As the video shows, when you run the application the symbol name is only printed once per tap on the row.
Case 3: Using NavigationLink(value:)
iOS 16 only.
iOS 16 introduced the concept of a NavigationLink(value). It is used similar to the existing versions, however rather than specifying a destination you provide a value. You then add .navigationDestination
modifier that determines which view is shown.
Below is the implementation of the HomeView.swift
file using the new NavigationLink(value)
.
As you can see, the ForEach
loops over the sfsymbolsArray
, and passes each sfsymbol to the NavigationLink(value: sfsymbol)
the label for the NavigationLink
is passed as the trailing label closure Label(sfsymbol.symbolName, systemImage: sfsymbol.symbolName)
.
The actual navigation takes place in the .navigationDestination
modifier. The .navigationDestination(for:)
accepts a class type. For our needs we're using .navigationDestination(for: Symbols.self) and then the sfsymbol
is passed to our SymbolView to display.
This allows you to have more than one type of class in a NavigationLink(value:) and then you would provide additional .navigationDestination(for:) modifiers for each of the class types.
But, what about .task
I've purposefully left out of discussion of the .task modifier because it's intended to perform an asynchronous task before a view appears. Fetching data from a JSON back end, or an image from the web, for example.
It's related to .onAppear
, but only tangentially. They both are fired when the views are first shown, but have different uses.
I'd love to hear your comments. Feel free to reach out to me at @sanguish on Mastodon.