SECRET OF CSS

Exploring SwiftUI Coordinators. Recycle a famous pattern that was… | by Pedro Alvarez | Aug, 2022


Recycle a famous pattern that was formerly designed for an imperative code base and reuse it to handle some declarative UI.

Photo by Swansway Motor Group on Unsplash

You may be already familiar with the Coordinator Design pattern first designed for the UIKit framework. Basically, it consists of delegating the navigating responsibility to another layer known as Coordinator .

This class holds a reference to the scene’s viewController or its navigationController , which allows pushViewController and present operations in order to have multiple navigation methods and passing the proper data between contexts.

We desire to do the same with SwiftUI, but problem is that in this framework everything regarding UI is declarative and we deal with value types(structs). So we cannot have a reference that we can directly ask to change context, the navigation is done in a declarative way by establishing NavigationViews and NavigationLinks which trigger some change as long as we manipulate its state data which is passed by input to these components. First things first, let’s dive into SwiftUI navigation primitives:

Like the UIKit, in SwiftUI you may change context in two ways: by pushing or modally. In UIKit you could use pushViewController in your navigationController in order to place a new ViewController into your stack or present if you wanted a modal ViewController. You have the same two ways to do that in SwiftUI but the mechanism is pretty different as we will see now

1. NavigationLinks

In SwiftUI, when we want a stack to hold multiple views presented sequentially, you have NavigationView to keep all your content. It’s like the former UINavigationController , but now instantiated in a declarative way. Inside this container, you may declare NavigationLinks that establish what are the possible routes you may go from the current context.

If you want a NavigationLink to work, it should be placed inside a NavigationView . Forgetting that is like trying to access a viewController‘s navigationController that is nil, what will not allow any navigation.

Basically, the navigation links force a change of context once its input binding boolean is true or if some tag is assigned with some value that corresponds to a custom view.

I will not cover details about it since the point of this article is to show a Coordinator layer in a declarative framework, so we shall go ahead.

2. Sheet

It’s a SwiftUI modifier that forces a new screen to be presented modally upon your current one once its binding boolean input becomes true or some input Identifiable is changed.

When we are doing some navigation to a new scene in SwiftUI, we simply declare the possible routes via NavigationLink or sheet modifier, differently from UIKit, where we used to imperatively “command” our navigationController to push a new screen into the stack. As we don’t have a reference to our NavigationView , we need some binding variables to control when is the right time to change screens. Said that, we need to split our Coordinator into two sublayers:

  1. CoordinatorView: Where we insert our NavigationView and establish our possible routes via NavigationLinks our with sheet modifier. Also, we need to insert our content view, the central component of our scene inside this CoordinatorView.
  2. CoordinatorViewModel: The layer where we place the logic data that shall trigger our change of context. As we are working with MVVM, don’t get confused by the scene ViewModel, which deals with the main scene logic, this one deals with navigation logic.

This is what our scenes may look like:

1*hZrWMiCcOyYhO2QPcDnkfQ

For short, our CoordinatorView has some NavigationView and coordinating directives, which are controlled by some binding logic within our CoordinatorViewModel . The CoordinatorView presents our View inside, which is also controlled by its own ViewModel and sends some user events to it, which may trigger a change of context to the CoordinatorViewModel , which does some navigation logic that forces the navigation in the CoordinatorView . Easy, right? Said that, Let’s create a new project on Xcode.

Open Xcode and create a new SwiftUI project. You may name it with something like CoordinatorSample. This is the folder structure we are gonna create:

Let’s start by creating some common models we need:

First, create this protocol at someplace. Basically, this will be an enum responsible for defining a single navigation route. We will check that in our first scene.

Create a new SwiftUI scene and call it Scene1CoordinatorView :

Here we have a just a View that establishes a NavigationView that wraps our input content(the main scene itself) together with a loop through all possible navigation cases(NavigationItem) in order to attach its own nextView to the NavigationLink destination. With this, we are allowing a route to each of those views for the giver NavigationItem . Since our NavigationItem is Identifiable and Hashable , we are allowed to tag our links with this type.

Repair that our CoordinatorView has a reference to Scene1CoordinatorViewModelProtocol instance. We are about to talk about it right now.

Create a new class inside our Scene1 folder and call it Scene1CoordinatorViewModel :

You may be confused with so many content, so let me explain each part:

  1. We have two NavigationItem concrete types, both of them are enums: Scene1NavigationItem which shall be attached to a NavigationLink and Scene1SheetItem , where will define the View to be presented by a sheet . Each enum case corresponds to a new context that may be routed by our scene. For each case, the nextView method returns a new scene. In the case, the Scene2Factory instantiates a new context, a coordinator view for the Scene2.

2. We have two protocols: Scene1CoordinatorViewModelProtocol which is the interface our Coordinator View will see it and Scene1CoordinatorProtocol , which our ViewModel will see through. navigateTo method receives a Scene1NavigationItem and present receives a Scene1SheetItem . There is a property for each of them inside our concrete class, so when the respective item is assigned, it updates our Coordinator View and the navigation is triggered by a NavigationLink , if a new Scene1NavigationItem is assigned or by sheet if a Scene1SheetItem is assigned.

Since the hash of a Scene1NavigationItem is computed also by its associated value, we shall save each new case into an array each time a new value comes , so we subscribe to our navigationItem publisher and append this item to our array. In order to save memory, each time we instantiate our scene we reset the values.

Let’s now create our view. It consists only of a TextField that passes some value to the next scene. We save our binding value inside the viewModel.

1*X3LXG2Qhtw5URHOE KdDzA

As you can see, the first button navigates to scene 2 via NavigationLink (check our Coordinator View) and the second navigates to scene 3 via sheet . Ok then, let’s talk about our ViewModel

This is our standard ViewModel, which I am calling by Logic ViewModel in order to differentiate from the CoordinatorViewModel . Its main tasks are holding the values that may change the View and answer for user interactions as well as triggering a change of context into the CoordinatorViewModel . Take a look:

As you can see, the SceneViewModel implements a protocol that holds a text String that is bound to our TextField in our View and methods that are triggered by our user interaction. Each one creates a new NavigationItem case and sends it to the CoordinatorViewModel to be handled.

It works exactly as the former ViewModel we are already used to.

We are adopting the Factory design pattern to build all of our scenes in a way of binding each of the layers and secure architecture integrity. Just create this enum in our Scene1 folder:

Since we are just navigating from Scene1 to both Scene2 and Scene3 we are not aiming to rebuild the scene structure to the other scenes, so, just create a new View for them which is going to just present a Text that was passed from origin scene:

1*WOZI20EV3Sf23cVAuhCyPg
1*dHyTDEVobeWC4DP60tOfvw

The viewModels can just hold the values that were passed from Scene1:

We just created a new project that holds an initial scene with a TextField whose value is passed to both scenes 2 and 3 via a Coordinator View Model, which controls whenever a change of context should be triggered in the Coordinator View. The final touch we shall check is this line of code:

Now you can just run the project and check the navigation magic:

1*BVa 2N1d4MVAMYTC40h4aQ

I didn’t share all the files from this project since my goal was to explain how to separate the navigation logic into two special layers(Coordinator) in Scene1 and send them forward.

Here is the link to this sample project on Github:

In this article, we explained how we could recycle a famous pattern that was formerly designed for an imperative code base and reuse that to handle some declarative UI.

We wrap our main scene in a NavigationView together with some links and sheet modifiers which are only enabled when some logic is achieved in our Coordinator ViewModel.

I hope this new MVVM-C architecture fits your future projects and you enjoyed 😉



News Credit

%d bloggers like this: