David Gary Wood

about
writing
apps
podcast
videos

SwiftUI - Router Pattern

May 5, 2021

SwiftUI is fantastic. I’ve fallen in love with how rapidly I can pull together a UI for iOS. The declarative and responsive nature of it, enables some really elegant code.

However, I quickly found that there’s a few ‘edges’ to the experience.

Dependency Injection

I like to divide business logic into view model objects, that my SwiftUI views read from. Those objects have dependencies (data managers, the app’s networking layer, etc).

I felt like SwiftUI was forcing a pattern of creating the VM’s in the view itself (@StateObject / @ObservedObject).

This can then require the dev to inject dependencies from the view, using SwiftUI’s Environment. Or, we can end up using patterns like Singleton builder objects to give things to the view models.

Another responsibility that SwiftUI gives to the view is navigation. Via NavigationLinks, TabViews, and Sheets, our views aren’t just responsible for their own content on the screen.

It starts to feel like the view is doing an awful lot.


Why fight it?

When working on team projects, my teams tend to require a few things. Projects need to have design patterns that enable them to be:

SwiftUI’s tightly coupled navigation, dependency injection and data transmission running through the view layer, disrupts these things.

Previous experience

When working on UIKit based projects, I have these same requirements for design patterns. I’ve used the Coordinator pattern, along with either separate Xibs for UI or programmatic UI and Snapkit, and MVVM to separate out business logic.

There’s a well trodden ground here. I have found these approaches help to split work up across developers in teams. The result is a codebase that is relatively easy for someone to contribute to, pretty soon after they join the team.

With all this in mind, I set about trying to figure out what an equivalent would look like for SwiftUI.

I decided I’d call any object in charge of flow, a Router.

The Objects

I started sketching things out on my home office’s whiteboard:

Router, View Model, and View objects listed on a whiteboard

If I want things to have single, or at least specific responsibilities, then this seemed like a good place to start!

Laying this out, gave me:

Router
* Creating Views
* Creating ViewModels
* Creating NavigationViews / TabViews
* Holding state to dictate screen flow
* Reacting to state to change screen flow
* Creating child Routers

View Model
* Carrying State for the view
* Sending data to dependencies
* Receiving data from dependencies (via bindings or delegation)
* Application Business logic

Screen - a type of view
* Sending data from user interactions to the View Model, to change state.
* Talking to the router to receive new views (I.e for NavigationLink destinations)

View
* A 'classic' SwiftUI view, receives bindings or maintains own state

An example project

You can find a github repository with the code for this post here .

To kick things off, I created xctemplate files, to help facilitate adding new Routers to a project, and new views.

You can add these templates by running the symlink-templates.sh shell script from the root of the repository.

Dependencies

All dependencies in the project are defined by protocol, and held in an object conforming to a protocol called Services that holds references to each one.

This is created when the app starts, and is transmitted from the first router, to all subsequent child routers and view models.

Adding our first router

Using the Router xctemplate, from a new file, a router can be added like so:

This gives us a basic Router, with a rootView containing a NavigationView and some text.

Adding a screen

A Screen is a SwiftUI view, that in this pattern, represents a full screen of content. This is essentially the same as the main view of a ViewController in UIKit.

This gives us a Screen file:

And an accompanying ViewModel:

The Screen has a protocol (seen here at line 3, protocol TestScreenRouter: AnyObject { …). This defines an interface for the screen to talk back to it’s Router.

This means that different Routers can create and present the same Screens, provided they implement that screen’s protocol.

Creating a screen in a Router

Creating a Screen in a Router, can look as follows:

Here we create the Screen’s view model, give it our Services dependency class. Then we can create the view, and give it the view model.

Conditional flow in Routers

An example of conditional flow can be found in AppRouter.swift

It’s rootView function, switches over it’s screen variable to choose whether to show the login screen or the main app.

NavigationLinks have to be held in our Screens. We can’t quite take this out of SwiftUI’s control. Our Router stil has responsibility for creating the view we’re navigating to though. To see an example of this look at the AccountScreen

Here, the AccountScreen talks to it’s router, via it’s protocol, to obtain the detail screen, when the NavigationLink is activated.

The AccountRouter, can create and return the detail screen like so:

Conclusion:

When this is all put together, our global dependencies to track our app’s state. In the example, the Login state is tracked in the LoginManager. Our AppRouter binds itself to that state, and chooses to respond to changes in that state accordingly. We don’t need to transmit bindings and data up and down our view hierarchy manually.

Our Views, represented by Screens, are coupled only with their own ViewModels. They can be moved between Routers with relative ease.

Our Screens, are, essentially just SwiftUI views. I’m not using any real ‘magic’ or secret source here.

This provides an approach that offers a simple structure, to enable our separation of concerns.

Ultimately I’d love for Apple to provide a concept of a Router, or Flow controller that we can just use. I’m hopeful that if that happens, this fairly lightweight structure will mean I can refactor relatively painlessly!

If you have any thoughts about this then I’d love to hear from you - email or Twitter !

Where to find the code:

You can find a github repository with the code for this post here .


Let's get in touch

Drop me a line.


twitter
RSS