Routing in SwiftUI
By: Ihor Vovk
Tags: Programming, Swiftui, Xcode, Mobile, Swift

1. Router With Trigger Views

Presenting view

The presenting view is initialized with a router that knows how to navigate to the presented view. The router provides a trigger view that, being inserted, can perform the desired navigation in response to user interactions.

Using PresentingRouterProtocol instead of the actual router class allows us to change the navigation logic easily and to initialize the PresentingView with a mock router for tests.

We should wrap the router property with @StateObject (available from iOS 14). This is because the navigation state resides in a router, and the view should update on the state changes. Also, in contrast to @ObservedObject , this wrapper avoids recreating a router every time the owner view is updated. It’s crucial for our implementation of push navigation based on NavigationLink .

Router protocols

There are two protocols. The first one is used by routers that can navigate to other views.

NavigationState will be implemented as a structure in subclasses. It’ll contain state variables for all possible navigation routes. binding is a convenient function used to get bindings for the existing state variables.

The other protocol is used for routers of views that were presented modally or pushed on a navigation view. It allows us to navigate back.

Usually, routers will conform to both the PresentingRouter and the PresentedRouter protocol.

Router implementation

A router conforming to PresentingRouterProtocol should be able to navigate to the details view. So its navigationState should contain a variable for this route.

The presentDetails function uses the binding function from the NavigatingRouter protocol extension to provide access to the corresponding state variable, both to the router of the destination view and to the returning trigger view. The destination router will use this binding for dismissing. And the trigger view will use the binding to trigger navigation.

Trigger views

A router returns trigger views that perform actual navigation in response to user actions. We’ll review only two of the most useful types of trigger views, although there can be more of them. Both take a content view and transform it into a resulting button that can navigate to the specified destination view.

The first one is SheetButton , used in the example above. It implements modal navigation with the SwiftUI .sheet modifier.

The second one is NavigationButton . It provides push navigation implemented with the NavigationLink inside.

We use the NavigationLink initializer with the isActive binding here since we want to keep the navigation state in a router.

Having the same initialization parameters in SheetButton and NavigationButton makes it easy to change the type of navigation from modal presentation to push navigation — and vice versa.

BasePresentedRouter class

Dismissing functionality is very common. So we’ll also introduce a simple base router class.

Presented view

The details view can only be presented and doesn’t perform any other kind of navigation. Because of this, we’ll use the base BasePresentedRouter router without subclassing here.

We can declare the router property without a wrapper in PresentedView because we use it only for dismissing, so the view doesn’t need to listen to the router’s state changes.

We hide the view when the user taps on its text.