How do you save and restore a NavigationPath, and what can go wrong?
Predict first
What does the path encode and restore?
import SwiftUI
enum Route: Hashable, Codable { case product(id: Int), settings }
struct Screen: Hashable { let id: Int } // Hashable but not Codable
var path = NavigationPath()
path.append(Route.product(id: 7))
path.append("search")
path.append(Route.settings)
print("count:", path.count)
let data = try JSONEncoder().encode(path.codable!)
let text = String(decoding: data, as: UTF8.self)
print("type names stored:", text.contains("Route"), text.contains("Swift.String"))
let again = try JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data)
print("restored count:", NavigationPath(again).count)
var mixed = NavigationPath()
mixed.append(Route.settings)
mixed.append(Screen(id: 1))
print("with a non-Codable element, codable is:", mixed.codable == nil ? "nil" : "non-nil")
- count 3 / type names stored / restored 3 / non-Codable gives nil
- only the values are stored
- a non-Codable element is skipped
Mental model. The saved path is a list of tickets with the venue’s name printed on them: if the venue renames itself, old tickets may still scan but take you somewhere unexpected.
A NavigationPath is a type-erased stack of Hashable values. It can produce a CodableRepresentation through path.codable, which is nil unless every element is Codable (the check appends a Hashable-only Screen and gets nil). The encoded JSON stores, for each element, its type name and its encoded value (the check finds Route and Swift.String in the text), and decoding rebuilds the elements by looking the type up by name: three elements in, three out. So a persisted path is a contract with your type names: renaming or moving a route type, or changing its coding, changes what old data means. In the check, decoding data whose type name had been altered did not throw and still reported three elements, which I did not investigate further, so do not assume a stale path fails loudly.
The reliable pattern: keep routes as small Codable enums with ids (not models), save the path in @SceneStorage or a file when the scene goes to background, decode defensively (catch errors, fall back to an empty path), version the format if routes evolve, and validate each restored route against current data before showing it (a deleted product id should pop, not crash). A typed array path ([Route]) is simpler to encode and migrate when the stack has one destination type. Pushing deep links is the same operation: append values to the path from one router object.
Encoding stores type names and values and needs Codable elements.
Common traps
- Putting non-Codable values in the path and calling
.codable!. It is nil. - Trusting restored routes. Validate them against current data.
They might follow up with
- Where do you store it?
@SceneStoragefor per-scene restoration, a file for sign-in state. - Typed array or
NavigationPath? Typed array when there is one route type.
Related questions
- 8.6.2 How does NavigationStack work, and how do NavigationPath and navigationDestination fit together?
- 8.4.2 What is the App protocol, what are scenes, and what does scenePhase tell you?
- 15.2.1 What is a coordinator or router, and how does it compare with SwiftUI’s NavigationStack for deciding who owns…
← What type does a ViewBuilder body actually produce for modifiers, if…