GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

Handling navigation the smart way with navigationDestination()

Paul Hudson    @twostraws   

In the simplest form of SwiftUI navigation, we provide both a label and a destination view in one single NavigationLink, like this:

NavigationStack {
    NavigationLink("Tap Me") {
        Text("Detail View")
    }
}

But for more advanced navigation, it's better to separate the destination from the value. This allows SwiftUI to load the destination only when it's needed.

Doing this takes two steps:

  1. We attach a value to the NavigationLink. This value can be anything you want – a string, an integer, a custom struct instance, or whatever. However, there is one requirement: whatever type you use must conform to a protocol called Hashable.
  2. Attaching a navigationDestination() modifier inside the navigation stack, telling it what to do when it receives your data.

Both of those are new, but to begin with you can ignore the Hashable requirement because most of Swift's built-in types already conform to Hashable. For example, Int, String, Date, URL, arrays, and dictionaries already conform to Hashable, so you don't need to worry about them.

So, let's look at the navigationDestination() modifier first, then circle back to look at Hashable in more detail.

First, we could make a List of 100 numbers, with each one being attached to a navigation link as its presentation value – we're telling SwiftUI we want to navigate to a number. Here's how that looks:

NavigationStack {
    List(0..<100) { i in
        NavigationLink("Select \(i)", value: i)
    }
}

Now, that code isn't quite enough. Yes, we've told SwiftUI we want to navigate to 0 when "Select 0" is tapped, but we haven't said how to show that data. Should it be some text, a VStack with some pictures, a custom SwiftUI view, or something else entirely?

This is where the navigationDestination() modifier comes in: we can tell it "when you're asked to navigate to an integer, here's what you should do…"

Modify your code to this:

NavigationStack {
    List(0..<100) { i in
        NavigationLink("Select \(i)", value: i)
    }
    .navigationDestination(for: Int.self) { selection in
        Text("You selected \(selection)")
    }
}

So, when SwiftUI attempts to navigation to any Int value, it gives us that value in the selection constant, and we need to return the correct SwiftUI view to show it.

Tip: If you have several different types of data to navigate to them, just add several navigationDestination() modifiers. In effect you're saying, "do this when you want to navigate to an integer, but do that when you want to navigate to a string."

That works great for lots of data, such as navigating to strings, integers, and UUIDs. But for more complex data such as custom structs, we need to use hashing.

Hashing is a computer science term that is the process of converting some data into a smaller representation in a consistent way. It's commonly used when downloading data: if you imagine downloading a movie on your Apple TV, that movie might be 10GB or so, but how can you be sure every single piece of data got downloaded successfully?

With hashing, we can convert that 10GB movie into a short string – maybe 40 characters in total – that uniquely identifies it. The hash function needs to be consistent, which means if we hash the movie locally and compare it to the hash on the server, they should always be the same, and comparing two 40-character strings is much easier than comparing two 10GB files!

Obviously there's no way to unhash data, because you can't convert just 40 characters back into a 10GB movie. But that's okay: the main thing is that the hash value for each piece of data ought to be unique, and also consistent so that we get the same hash value for the movie every time.

Swift uses Hashable a lot internally. For example, when you use a Set rather than an array, everything you put in there must conform to the Hashable protocol. This is what makes sets so fast compared to arrays: when you say "does the set contain this particular object?" Swift will compute the hash of your object, then search for that in the set rather than trying to compare every property against every object.

If all this sounds complicated, remember that most of Swift's built-in types already conform to Hashable. And if you make a custom struct with properties that all conform to Hashable, you can make the whole struct conform to Hashable with one tiny change.

As an example, this struct contains a UUID, a string, and an integer:

struct Student {
    var id = UUID()
    var name: String
    var age: Int
}

If we want to make that struct conform to Hashable, we just add the protocol like this:

struct Student: Hashable {
    var id = UUID()
    var name: String
    var age: Int
}

Now that our Student struct conforms to Hashable it can be used with both NavigationLink and navigationDestination() just like integers or strings

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.

Learn more here

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.3/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.