Introduction

One of the most interesting concepts in iOS programming is, in my opinion, the Coordinator – an entity that is responsible for managing an app’s flow. Its typical responsibilities include configuring, presenting and dismissing View Controllers as well as configuring other Coordinators, often called children. If you are not familiar with Coordinators, please have a look at this great post by Soroush Khanlou which, AFAIK, is the first to propose using them within the context of iOS programming.

The obvious advantage of Coordinators is that the logic of managing an app’s flow is taken away from the View Controller, thus solving a major issue that iOS developers have faced for a long time – the View Controller as a bloated, God-like, know-it-all object. By using Coordinators, a View Controller does not need to know for example whether it is supposed to initialise, configure and present another View Controller. Neither does it have to know what to do if another View Controller that itself is already presenting is dismissed. All that stuff is being taken care of by the Coordinator.

So, Coordinators are very helpful – and popular. In fact, they are so popular that they are often added to one of the most common design patterns, MVVM (Model-View-View Model), leading to another version of it, MVVM-C (Model-View-View Model + Coordinator). However, as the app grows, its flow can become complicated and a set of Coordinator might not be enough. Therefore, a case can be made for extending the pattern with a couple of new concepts that can assist with managing the complexity. Enter Scene and Scene Navigator.

Scene

In that context a Scene can be defined as any substantial, distinct app UI state, with at least one View Controller that is presented in some way. For example, a new View Controller pushed onto a Navigation stack can be a Scene. A View Controller that is presented modally can also be a Scene. Embedded controllers, as well as alert controllers, are a somewhat different story, since a lot depends on the context – sometimes they do define a Scene, sometimes they don’t. A Scene change (going from one Scene -origin- to another Scene -destination-) can be defined as a Route.

Scene Navigator

A Scene Navigator in an entity that manages an app’s Scene state. In its simplest form it keeps track of the app’s current Scene, which can be instructed to change and informs a set of observers about the changes. Notice that a Scene Navigator itself does not implement any Scene changes. This is left to the Coordinators. Thus, a simple set of relationships between a Coordinator and a Scene Navigator would be the following:

  • A Coordinator can own a reference to the app’s Scene Navigator.
  • The Coordinator can instruct the Navigator to go a particular Scene.
  • The Coordinator can observe a Navigator’s Scene changes (Routes) and react to those that are relevant to it.

Example

The following example demonstrates one possible application of the MVVM-C + Scene/Scene Navigator pattern described above. You can download the full code here (Xcode 12.5, Swift, iOS 13.0+).

The example app consists of three simple scenes (named Home, Detail and Hello World), a Scene Navigator and four Coordinators: three Coordinators managing each one of the scenes (View Controllers and View Models included), plus the App Coordinator. Every screen within a Scene contains a number of UI elements which, upon user interaction, create a Route that establishes a new visible Scene. Every Route goes through the Scene Navigator and is observed and intercepted by the Coordinators who react to it by presenting/dismissing Scenes accordingly. The app’s root View Controller is a Navigation Controller whose root View Controller is HomeSceneViewController, the main View Controller of the Home Scene.

Scenes

Home

Detail

Hello World

Flow/Routes

Home to Detail (push)

Detail to Home (pop)

Detail to Hello World (push)

Hello World to Detail (pop)

Hello World to Home (pop)

Home to Hello World (modal)

Hello World to Home (modal)

There are three Scenes and seven Routes in the app, which are managed via the Coordinators, alongside the Scene Navigator. Let’s have a closer look at the latter:

import Foundation
import Combine

class SceneNavigator {
    
    enum Scene {
        
        case home
        case detail
        case helloWorld(data: HelloWorldSceneNavigationDTO)
    }
    
    enum ScenePresentationStyle {
        
        case push
        case modal
    }
    
    @Published var route: (to: Scene, from: Scene) = (.home, .home)
    private var lastScene: Scene = .home
    
    func go(to scene: Scene) {
        
        nextScene = (to: scene, from: lastScene)
        lastScene = scene
    }
}

Here, the app’s Scenes are represented by Swift Enumeration values, taking advantage of powerful features such as associated values – plus a single source of truth for all Scenes (plus some cool naming). Regarding associated values, take for example the case of the Scene HelloWorld which contains additional information in the form of helloWorldSceneNavigationDTO:

import Foundation

struct HelloWorldSceneNavigationDTO {
    
    let presentationStyle: SceneNavigator.ScenePresentationStyle
}

In that way, the Coordinator that is managing this Scene, HelloWorldSceneCoordinator, is always aware of how the Scene’s main (in this case only) View Controller, HelloWorldViewController, should be presented.

The class SceneNavigator also contains an Observable (Published in Combine jargon) property called route which emits an event every time there is a scene change (Route). The event contains two values, the new Scene that the app should navigate to (Route destination), as well as the app’s current -visible- Scene (Route source).

SceneNavigator also contains a function, go(to scene:) which instructs the Scene Navigator to change the app’s current Scene (and store the last scene to lastScene), thus forming a new Route which is emitted by route as mentioned above.

Every Coordinator apart from the AppCoordinator which does not manage any Scene and HelloWorldCoordinator which does not manage any Route, is subscribed to the Scene Navigator’s route, intercepts every event and reacts to those that are of interest to it. For example, the Route handling of HomeSceneCoordinator is:

func handle(scene: SceneNavigator.Scene, source: SceneNavigator.Scene) {
        
        switch (scene, source) {
        
        case (.detail, .home):
            presentDetailScene()
        
        case (.helloWorld(let data), .home):
            presentHelloWorldScene(data: data)
        
        case (.home, let source):
            presentHomeScene(source: source)
        
        default:
            ()
        }
}

And that of the DetailSceneCoordinator:

func handle(scene: SceneNavigator.Scene, source: SceneNavigator.Scene) {
        
        switch (scene, source) {
        
        case (.helloWorld(let data), .detail):
            presentHelloWorldScene(data: data)
            
        case (.detail, .helloWorld):
            rootViewController.delegate = previousNavigationControllerDelegate
            children.removeAll()
            
        default:
            ()
        }
}

Therefore, when for example the user is on the detail Scene and goes to helloWorld, the DetailSceneCoordinator intercepts the Route, checks whether it should handle it and, if yes, it configures and presents the helloWorld Scene with the additional data provided by its associated value (HelloWorldSceneNavigationDTO). The same happens with any other possible Route in the app.

Summary

  • Coordinators can be used along Scenes and Scene Navigators in a MVVM-C pattern, in order to help reducing an app’s flow complexity.
  • Representing Scenes as Swift Enumeration values can offer advantages such as passing relevant information in the form of associated values.
  • Scene state can be managed by a Scene Navigator.
  • By observing Scene changes, emitted by the Scene Navigator, Coordinators can react to and handle the Scenes that are relevant to them.

Thanks for reading!