Which components are included in a coordinator?
I think that an architectural pattern should work more as a guideline rather than a strict blueprint. However, I also do think it is important to have a general outline of which components fit into the coordinator pattern, before discussing the topic further.
The core component of the coordinator pattern is often referred to as the app coordinator. The app coordinator can be seen as an overarching parent coordinator, responsible for initializing child coordinators for our app’s different main flows.
Each child coordinator initializes and holds a reference to a View and the View’s ViewController.
The focus of this blog post is to outline what I see fits into these different coordinator components, in terms of responsibility and in terms of the separation of a parent coordinator and a child coordinator. If you want to read more about the relationship between the different coordinators, read my earlier post about coordinators here.
What should be included in a coordinator?
In general, a common pattern that can be seen in a coordinator is that it contains the following basic components and functions:
- A UIViewController often named root, which acts as a root view controller
- A UINavigationController
- An array or a dictionary often named childCoordinators which contains the different child coordinators that a coordinator may hold
- A start and a stop function (more on these two below)
An app - and a parent - coordinator’s responsibility
Apart from containing the variables and methods outlined above, the app coordinator also includes a UIWindow variable which is the main window for displaying the app’s content.
An app coordinator is usually initialized by the app’s AppDelegate and is responsible for initializing and setting the app’s root view controller (by setting the UIWindow’s rootViewController variable). It is also the app coordinators responsibility to set up the required child coordinators for the app’s different main screens and their flows.
In an app that has multiple screens, which have their own coordinators, the app coordinator functions as a parent coordinator, where its start function is responsible for initializing different child coordinators, each responsible for a specific screen and for this screen’s flows. When a parent coordinator initializes these child coordinators, it may provide it with a root ViewController by passing in its own root variable. Alternatively, when the parent coordinator is the app coordinator, it may assign one if the childCoordinators’ ViewController to be the app’s root ViewController. Once the child coordinator is initialized, the parent coordinator may be responsible for calling the child’s start function (see below section for more information about what the child coordinator’s start method does). A parent coordinator’s start functionality is visualised in this diagram:
A parent coordinator’s stop method is responsible for de-initializing a specific child coordinator in order to prevent memory leaks.
A child coordinator’s responsibility
Generally, a child coordinator contains a UIViewController which is being presented to the user. The child coordinator’s start function is responsible for initializing this ViewController, by providing its required resources through dependency injection. Once this ViewController is initialized, the child coordinator presents the ViewController. If the ViewController requires a Model object or a ViewModel, it’s the coordinator’s responsibility to provide these objects, or alternatively, provide the ViewController with a way to retrieve this data - for example by passing in a service function (which for example accesses our Core Data database or executes network requests) to the ViewController.
One of the core responsibilities of a child coordinator is to provide its ViewController with navigational logic. For example, if we display a list of items in a UITableView, and each of the TableView cells are tappable, it’s the child coordinator’s responsibility to provide the TableViewController with the navigational logic of how to route from the TableView to the detail view. This can either be done by delegation or by passing the navigational functionality to one of the ViewController’s local values when initializing the ViewController.
A child coordinator’s stop function is responsible for de-initializing the ViewController it holds and potentially de-initialize itself.
A child/parent coordinator
Note that the above description of a coordinator distinguishes between a parent and a child coordinator, where the app coordinator is a parent coordinator. I decided to outline the responsibility of the two coordinator types as two separate entities in order to make a clear distinction between the different coordinator types’ responsibilities. However, when I’ve used the coordinator pattern, the separation is seldom this clear. In many cases, a child coordinator may also be a parent coordinator. For example, if we initialize a child coordinator for our main view from our app coordinator, the main view may also have different sub-views with different navigation flows, these sub-views may be initialized and handled through two different child coordinators initialized and held by the main view coordinator. In this scenario, we would have three layers of coordinators, the app coordinator (a parent coordinator) the main view coordinator (a child coordinator, as well as a parent coordinator) and the main view’s two child coordinators.
When I have used the coordinator pattern, I haven’t found it necessary to determine a strict rule around whether a coordinator needs child coordinators or not. I found that it’s something to decide on case-by-case bases. If a coordinator’s view should navigate to a different view, which in its turn requires navigational logic, it may be worthwhile to include an additional layer of coordinator logic. If the navigational logic is as simple as just displaying an alert view, it might not be necessary to include a coordinator for this simple logic. What level of complexity we wish to apply to our coordinator pattern may be decided on by considering the navigational needs for a specific view in a pragmatic way.