LAST CHANCE: Save 50% on all my Swift books and bundles! >>

How to save NavigationStack paths using Codable

Paul Hudson    @twostraws   

You can save and load the navigation stack path using Codable in one of two ways, depending on what kind of path you have:

  1. If you're using NavigationPath to store the active path of your NavigationStack, SwiftUI provides two helpers to make saving and loading your paths easier.
  2. If you're using a homogenous array – e.g. [Int] or [String] – then you don't need those helpers, and you can load or save your data freely.

The techniques are very similar, so I'll cover them both here.

Both rely on storing your path outside your view, so that all the loading and saving of path data happens invisibly – an external class just takes care of it automatically. To be more precise, every time our path data changes – whether that's an array of integers or strings, or a NavigationPath object – we need to save the new path so it's preserved for the future, and we'll also load that data back from disk when the class is initialized.

Here's how that class would look when our path data is stored as an array of integers:

@Observable
class PathStore {
    var path: [Int] {
        didSet {
            save()
        }
    }

    private let savePath = URL.documentsDirectory.appending(path: "SavedPath")

    init() {
        if let data = try? Data(contentsOf: savePath) {
            if let decoded = try? JSONDecoder().decode([Int].self, from: data) {
                path = decoded
                return
            }
        }

        // Still here? Start with an empty path.
        path = []
    }

    func save() {
        do {
            let data = try JSONEncoder().encode(path)
            try data.write(to: savePath)
        } catch {
            print("Failed to save navigation data")
        }
    }
}

If you're using NavigationPath, you need only four small changes.

First, the path property needs to have the type NavigationPath rather than [Int]:

var path: NavigationPath {
    didSet {
        save()
    }
}

Second, when we decode our JSON in the initializer we need to decode to a specific type, then use the decoded data to create a new NavigationPath:

if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
    path = NavigationPath(decoded)
    return
}

Third, if decoding fails we should assign a new, empty NavigationPath instance to the path property at the end of the initializer:

path = NavigationPath()

And finally, the save() method needs to write the Codable representation of our navigation path. This is where things diverge just a little more from using a simple array, because NavigationPath doesn't require that its data types conform to Codable – it only needs Hashable conformance. As a result, Swift can't verify at compile time that there is a valid Codable representation of the navigation path, so we need to request it and see what comes back.

That means adding a check at the start of the save() method, which attempts to retrieve the Codable navigation path and bails out immediately if we get nothing back:

guard let representation = path.codable else { return }

That will either return the data ready to be encoded to JSON, or return nil if at least one object in the path cannot be encoded.

Finally, we convert that Codable representation to JSON instead of the original Int array:

let data = try JSONEncoder().encode(representation)

Here's how the complete class looks:

@Observable
class PathStore {
    var path: NavigationPath {
        didSet {
            save()
        }
    }

    private let savePath = URL.documentsDirectory.appending(path: "SavedPath")

    init() {
        if let data = try? Data(contentsOf: savePath) {
            if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
                path = NavigationPath(decoded)
                return
            }
        }

        // Still here? Start with an empty path.
        path = NavigationPath()
    }

    func save() {
        guard let representation = path.codable else { return }

        do {
            let data = try JSONEncoder().encode(representation)
            try data.write(to: savePath)
        } catch {
            print("Failed to save navigation data")
        }
    }
}

Now you can go ahead and write your SwiftUI code as normal, making sure to bind the path of your NavigationStack to the path property of a PathStore instance. For example, this lets you show views with random integers attached – you can push as many views as you like, then quit and relaunch the app to get it exactly back as you left it:

struct DetailView: View {
    var number: Int

    var body: some View {
        NavigationLink("Go to Random Number", value: Int.random(in: 1...1000))
            .navigationTitle("Number: \(number)")
    }
}

struct ContentView: View {
    @State private var pathStore = PathStore()

    var body: some View {
        NavigationStack(path: $pathStore.path) {
            DetailView(number: 0)
                .navigationDestination(for: Int.self) { i in
                    DetailView(number: i)
                }
        }
    }
}
Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.2/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.