Key moments
00:57 - Setup/overview
01:25 - Comparison of pre-iOS16 navigation APIs with new
05:24 - Recipes for navigation
06:06 - Pushable Stack recipe, single stacks of view
10:43 - Multiple Columns recipe, columns showing progressively more info
14:12 - Multiple Columns With Stack recipe, Navigating between related information
18:04 - Persistent State
23:34 - Navigation tips/summary
Deprecation notice
All previous navigation APIs are deprecated:
NavigationViewis replaced by two new containers,NavigationStackandNavigationSplitView(more on this later)NavigationLinkstill exists, has a completely different API and behavior, and is no longer mandatory to use
New navigation APIs
Containers
with the new containers, we have a single binding for managing their stack state/path
this single binding represents all the values pushed onto the stack, think of it as an array of screens
The new
NavigationLinkAPIs append values to this bindingyou can directly mutate this binding yourself (just add/remove elements from this state/path array)
You can programmatically push/pop multiple screens at once
You can programmatically deep link
You can pop to the root view by removing all the items
Two new containers:
NavigationStack- for a single push-pop stackNavigationSplitView- for multi-column apps
automatically adapts into a single-column stack on iPhone, into Slide Over on iPad, Apple Watch and Apple TV
provides configuration options that let you:
customize column widths (see
navigationSplitViewColumnWidth(_:)and similar modifiers)control sidebar presentation and show/hide columns (see
NavigationSplitViewVisibilityand associated modifiers)
NavigationSplitView has two initializers:
To create a two-column navigation split view, use
init(sidebar:detail:)To create a three-column view, use the
init(sidebar:content:detail:)
NavigationStack(path: $path) {
NavigationLink("trigger", value: value)
}
NavigationSplitView {
List(model.employees, selection: $employeeIds) { employee in
Text(employee.name)
}
} detail: {
EmployeeDetails(for: employeeIds)
}NavigationLink
appends elements onto the stack it appears in
no longer accepts a
destinationparameter (we use the newnavigationDestination(for:destination:)modifier instead)is no longer needed, programmatically append things to the
$pathstate yourself instead
navigationDestination(for:destination:) modifier
declares the type of the presented data that it’s responsible for
takes in a view builder that describes what view to push onto the stack when a instance of that data is presented
NavigationStack {
List {
NavigationLink("Mint", value: Color.mint)
NavigationLink("Pink", value: Color.pink)
NavigationLink("Teal", value: Color.teal)
}
.navigationDestination(for: Color.self) { color in
ColorDetail(color: color) // The view to be pushed
}
.navigationTitle("Colors")
}Explanation: when a navigation link is tapped, a new
Colorvalue is appended to theNavigationStackinternal$pathstate,NavigationStackthen asks thenavigationDestination(for:destination:)modifier associated withColorfor the view to be pushed.
Every navigation stack keeps track of a path that represents all the data that the stack is showing
When the stack shows its root view, the path is empty
the stack also keeps track of all the navigation destinations declared inside it, or inside any view pushed onto the stack
if you’d like to use this path yourself (e.g., for programmatic navigation), use the new type-erasing
NavigationPathcollection where you can push values of different types
Example with explicit path:
struct ContentView: View {
/// The stack state.
@State private var path: [Int] = []
var body: some View {
// This navigation doesn't use NavigationLink! 🎉
NavigationStack(path: $path) {
NumberView(0, onGoToNextNumber: { path = [1] })
// 👇🏻 This modifier returns the view to be pushed for $path values of type Int
.navigationDestination(for: Int.self) { number in
NumberView(number, onGoToNextNumber: { path.append(number + 1) })
}
}
}
}
/// A view that displays a number and has a button triggering an action injected by
/// its parent view.
struct NumberView: View {
var number: Int
var onGoToNextNumber: () -> Void
init(_ number: Int, onGoToNextNumber: @escaping () -> Void) {
self.number = number
self.onGoToNextNumber = onGoToNextNumber
}
var body: some View {
Text("\(number)").font(.headline)
Button("Go to next number", action: onGoToNextNumber)
}
}Persistent state
encapsulate your navigation state into a
Codablemodeluse
SceneStorageto save and restore that staterestore data via
.task:
// Use SceneStorage to save and restore
@StateObject private var navModel = NavigationModel()
@SceneStorage("navigation") private var data: Data?
var body: some View {
NavigationSplitView { ... }
.task {
// restore state if present
if let data = data {
navModel.jsonData = data
}
// start an asynchronous for loop that will iterate
// whenever my navigation model changes. The body of
// this loop will run on each change, so I can use
// that to save my navigation state back to my scene
// storage data.
for await _ in navModel.objectWillChangeSequence {
data = navModel.jsonData
}
}
}Tips
Check Apple’s Migrating to New Navigation Types article
List,NavigationSplitView, andNavigationStackwere made to mix togetherput
navigationDestination(for:destination:)modifiers within easy reachstart with
NavigationSplitViewwhen it makes sense
Related
SwiftUI on iPad: Organize your interface
SwiftUI on iPad: Add toolbars, titles, and more
