SwiftUI Navigation: Links, Part 3

Episode #167 • Nov 8, 2021 • Subscriber-Only

Over the past weeks we have come up with some seriously powerful tools for SwiftUI navigation that have allowed us to more precisely and correctly model our app’s domain, so let’s exercise them a bit more by adding more behavior and deeper navigation to our application.

Links, Part 3
Introduction
00:05
Item validation and a custom color picker
03:13
Moving behavior to a view model
12:16
Next time: the point
37:11

Unlock This Episode

Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.

Introduction

So, this is all looking really promising. We have an incredible amount of power in deep linking and testing in our application. When we first started discussing navigation links earlier in this episode we mentioned that they are the most prototypical form of navigation, but also at the same time the most “complicated.”

However, we just introduced a pretty complex navigation link for editing an item, and even showed how we could gate the navigation based on asynchronous effects. This is some of the most complicated navigation you can do in an iOS application, and not only did we accomplish it pretty quickly, but we also got deep linking for free along the way. To contrast with modal sheets and popovers, it took us three very long episodes to develop the techniques that allowed us to model them in state and support deep linking.

Perhaps this form of navigation isn’t so complicated after all?

Well, it definitely is, but it’s come much easier to us thanks to all of the tools we built in previous episodes. The only reason we are able to model our navigation routes as a simple enum and implement a NavigationLink initializer that transforms binding of optionals into bindings of honest values is thanks to all the binding transformation helpers we have defined in past episodes.

So, we’re starting to see that by putting in a little bit of upfront work to try to put structs and enums on the same footing when it comes to binding transformational operators, we unlock powerful tools that help make navigation in SwiftUI a breeze. Just as dynamic member lookup allows us to slice of a binding for a piece of sub-state and hand it off to a child component, the .case transformation allows us to slice off a binding for a particular case of an enum and hand it off to a child component. And when you combine that with navigation tools such as sheets, popovers, and links, you instantly unlock the ability to drive navigation off of state.

But let’s push things even further. Right now the ItemView has no behavior. It’s decently complex, especially since we are transforming the item status binding into bindings for each case of the status enum. But even so, there’s no real behavior in the screen. We aren’t executing any asynchronous work or side effects. The SwiftUI view is just reading from bindings and writing to bindings.

This has worked well for us so far, but as soon as we want to introduce some behavior to the view, and do so in a testable way and in a way that allows for deep linking, we can no longer get away with using simple bindings. We must use an observable object. We’ve already got two observable object view models in this project: one for the inventory list feature and one for the row of the list.

We’re going to introduce yet another view model, this time for the item view. In order to justify this new view model we are going to add some complex behavior to the screen. We are going to add some validation logic to the form, and we’re going to make the color picker more robust by having it load additional colors from an asynchronous effect.

Let’s start by trying to implement these features without a view model and show where things go wrong.

This episode is for subscribers only.

Subscribe to Point-Free

Access this episode, plus all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Exercises

  1. In this episode we were able to swap out our custom sheet(unwrap:) and popover(unwrap:) helpers for the simpler sheet(item:) and popover(item:) modifiers that come with SwiftUI when moving from a binding to an observed object.

    However, we were unable to swap out our custom NavigationLink.init(unwrap:) helper, because no such equivalent API exists in vanilla SwiftUI. Why is that, and can you define a NavigationLink.init(item:) helper that does just that?

    Solution

    It is possible, and we can even leverage our helper to define it simply:

    extension NavigationLink {
      init<Value, WrappedDestination>(
        item optionalValue: Binding<Value?>,
        onNavigate: @escaping (Bool) -> Void,
        @ViewBuilder destination: @escaping (Value) -> WrappedDestination,
        @ViewBuilder label: @escaping () -> Label
      )
      where Destination == WrappedDestination?
      {
        self.init(
          unwrap: option,
          onNavigate: onNavigate,
          destination: { binding in destination(binding.wrappedValue) },
          label: label
        )
      }
    }
    

References

SwiftUI Navigation

Brandon Williams & Stephen Celis • Tuesday Nov 16, 2021

After 9 episodes exploring SwiftUI navigation from the ground up, we open sourced a library with all new tools for making SwiftUI navigation simpler, more ergonomic and more precise.

WWDC 2021: Demystifying SwiftUI

Matt Ricketson, Luca Bernardi & Raj Ramamurthy • Wednesday Jun 9, 2021

An in-depth explaining on view identity, lifetime, and more, and crucial to understanding how @State works.

Collection: Derived Behavior

Brandon Williams & Stephen Celis • Monday May 17, 2021

The ability to break down applications into small domains that are understandable in isolation is a universal problem, and yet there is no default story for doing so in SwiftUI. We explore the problem space and solutions, in both vanilla SwiftUI and the Composable Architecture.

Downloads