Skip to Content

iOS Coordinators

In our apps, we use the MVVM (Model-View-ViewModel) approach. This concept manages the interaction within a view controller, but who manages the view controllers?

The Coordinator!

More precisely, multiple coordinators manage almost all view controllers in our apps. Coordinators are arranged in a tree structure, each coordinator is responsible for certain UI flow (a bunch of view controllers) in the app.

Let start with a simple example. A registration flow with a single input per screen.

MVVM+C

This is an implementation of a registration coordinator which manages our registration flow. Each screen enters one value and adds information to our user object to complete the registration. The RegistrationCoordinator is a subclass of NavigationCoordinator. A NavigationCoordinator manages a UINavigationController. So our RegistrationCoordinator is completely in charge of the order and presentation of its view controllers. View controllers do not present the next view controller. View controllers are not responsible for presenting other view controllers, this is the job and responsibility of the coordinator. The view controllers only report to the coordinator (using a closure or delegate) if the user completed a task such as entering an email address.

class RegisterCoordinator: NavigationCoordinator {
	
	enum Step {
		case email
		case password
		case firstName
		case lastName
	}

	var didFinish: ((User) -> Void)?
	var didCancel: (() -> Void)?

	let steps: [Step] = [ .email, .password, .firstName, .lastName]

	func start() {
		showNext(step: .email)
	}

	func showNext(step: Step) {
		let viewController = createViewController(with: step)
		push(viewController, animated: true)
	}

	// ...
}

As mentioned before, coordinators are organised in a hierarchy. At the root, we have an AppCoordinator which manages all user flows in our app. A coordinator can have multiple child coordinators, which represent features of the app such as the RegistrationCoordinator shown above.

We have two kinds of base coordinators: NavigationCoordinator and TabBarCoordinator.

Schema

Reusable View Controllers

In our approach view controllers have a well-defined task and interface (= input & output). For example, the task of a TextInputViewController is simple: it takes a text input from the user and reports it by calling the didEnterText closure.

class TextInputViewController: UIViewController {
	
	// MARK: - Output

	var didEnterText: ((String) -> Void)?

	// MARK: - Input

	var viewModel: TextInputViewModel!

	class func create(viewModel: TextInputViewModel) -> Self {
		let viewController = UIStoryboard(.onboarding).instantiateViewController(self)
		viewController.viewModel = viewModel
		return viewController
	}	

	// ...
}

The coordinator which uses the TextInputViewController implements this closure and acts accordingly. In our registration example the coordinator would present the next step or finish the registration. That view controller does not know which view controller will be presented next as this is the responsibility of the coordinator. That way the TextInputViewController can be easily reused on multiple occasions across the app in different user flows.

Recap

At first, this concept may look a little over-engineered and complex. And to be honest, for a small and simple app it probably is. But in our experience simple apps usually do not stay simple, they grow! Using this approach at the start of a project eases the way for future updates. The clear structure and delegation of responsibility from the view controller to the coordinator makes a project easier to maintain and simpler to collaborate on well-defined UI flows or reusable view controllers.

Check out our coordinator implementation in our iOS Starter project on Github: https://github.com/allaboutapps/ios-starter

Additional resources

Categories: iOS