Reactive Coordinator Pattern (with Combine)

Ting Yi Shih (Peter Shih)
4 min readJul 12, 2024

--

Coordinator pattern is a common solution that we use to isolate navigation from view controllers.

Rather than the typical coordinator referenced by the view controller, such as mentioned in the article, this story will instead focus on the coordinator pattern in a reactive way, in which the coordinator will proactively subscribe the steps from the view model.

Note that it’s not my novel idea but inspired by RxFlow. I simply favor this pattern in my work and try to promote the idea in a simplified manner.

MVVM and Coordinator Pattern

It’s normally considered a good practice to separate the navigation (interaction with coordinator) from the data flow (interaction with view model) for granular organization and maintenance, which often implies better code re-usability.

We can have MVVM followed strictly if

  • A view controller interacts with its view model but nothing else. The VC gives input to the VM and takes the output from the VM.
  • The owner of the business logic is the VM. That said, the VC doesn’t decide but follows.

That makes VM the role to ask for navigation. Here comes the Coordinator:

  • Coordinator decides how and what VC to present (or dismiss).
  • Coordinator subscribes the VM to receive the steps and respond.

Reactive Coordinator Pattern

Suppose we have a Home Page, where we tap the login button to have the login page showed. A coordinated flow illustrated as follows:

In the reactive Coordinator pattern, the Coordinator subscribes the view model for the requested steps. The view model is the step provider. Below we use a simplified code to roughly picture how it works.

First: define the steps

Create a type that conforms to the Step protocol for communication within the coordinator.

enum HomeSteps: Step { // conform to the Step protocol

// to start the Home page
case launch

// to show the login page
case showLogin

}

Second: build the Coordinator

Here we subclass Coordinator— a base class that has the subscription to the step provider internally. Override the navigation method to define how it reacts to a step.

final class HomeCoordinator: Coordinator { // subclass Coordinator

// override the method to define how we respond to a step
override func navigate(to step: Step) -> Navigation {

switch step {

// when asked to launch the editor
case HomeSteps.launch:
let home = HomeViewController()
return .many([
// start the flow to subscribe the lifecycle of the root
// will call .endFlow if the root is deallocated
// this must be done on the first step of the flow
.startFlow(root: home),

// subscribe potential steps requested by the view model
.subscribeSteps(home.viewModel, presenting: home),
])

// when asked to show the login page
case HomeSteps.showLogin:
// we present the layout picker in response to the step
rootViewController.present(LoginViewController())
return .subscribeNoSteps

default:
return .undefined

}

}

}

where enum Navigation describes the action to be undertaken by the Coordinator:

/// Describe navigation to be undertaken by the Coordinator.
enum Navigation {
/// Start the coordinator and follow the lifecycle of the root
case startFlow(root: UIViewController)

/// Explicitly declare no steps to subscribe
case subscribeNoSteps

/// Subscribe the steps from the provider until the presented is deallocated
case subscribeSteps(StepProvider, presenting: UIViewController)

/// End the coordinator
case endFlow

/// Use this case when multiple navigations happen at the same time
indirect case many([Navigation])
}

Third: make the view model a step provider

Make your view model a step provider via the publisher of steps, which is subscribed by the Coordinator. The view model maps the input to steps emitted to the step publisher.

class HomeViewModel: StepProvider { // conform to StepProvider
private let stepSubject = PassthroughSubject<Step, Never>()
var stepPublisher: AnyPublisher<Step, Never> { stepSubject.eraseToAnyPublisher() }

// to bind the view controller
func bind(_ input: Input, subscriptions: inout [AnyCancellable]) {

// map the input to a step
// and emit to the step subject
input.tapLoginButton
.map { HomeSteps.showLogin }
.subscribe(stepSubject)
.store(in: &subscriptions)

}

}

Forth: bind the view controller to the view model

Finally, connect the view model to a view controller by creating the input to and obtain the output from the view model.

class CollageEditor: UIViewController {
let viewModel = CollageEditorViewModel()

func viewDidLoad() {
// forward the tap on the layout button
// as the binding input to the view model
let output = viewModel.bind(input, &subscriptions)
}

}

That’s pretty much the basic idea! If you are into a real implementation, find the full code example here: https://github.com/peteranny/ReactiveCoordinatorExample.

The Whole Picture

Coordinator Can Have Children

A coordinator represents one flow. Conceptually we can have nested flows, so a coordinator should be able to initiate a child coordinator. That implies a new case to the navigation:

enum Navigation {

/// Start a child coordinator with an initial step
case startChildFlow(Coordinator, with: Step)

}

A fully coordinated app is expected to start from a root coordinator, which navigates to the main flow or more sub-flows. Foreseeably, a possible coordination can look like:

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Ting Yi Shih (Peter Shih)
Ting Yi Shih (Peter Shih)

Written by Ting Yi Shih (Peter Shih)

Love exploring an elegant pattern that forms robust, maintainable, and understandable coding style.

No responses yet

Write a response