This the going to be the first post in a series on Advanced Coordinators. If you haven’t read the original post or the longer follow-up, make sure to check those out first. The series will cover a few advanced coordinator techniques, gotchas, FAQs, and other trivia. Let’s dig in.
I’m often asked how to migrate an app from using storyboards or per-view controller code-based flow to an app using coordinators. When done right, this refactoring can be done piecemeal. You will continuously be able to deploy your app, even if the refactoring isn’t complete.
To acheive this, the best thing to do is start from the root, which for coordinators is called the “app coordinator”. The app delegate holds on to the app coordinator, which is the coordinator that sets up all the view controllers for your app’s launch.
To understand why we start from the root of the app, consider the opposite. If we started from some leaf flow (like, say, a
CheckoutCoordinator), then something needs to maintain a strong reference to the coordinator so that it doesn’t deallocate. If the coordinator deallocates, none of its code can run. So, deep in an app, if we create a coordinator, something will have to hold on to it.
There are two ways to prevent this deallocation. The first option is to make a static reference. Because there will likely only ever be one
CheckoutCoordinator, it’ll be easy to stuff it in to a global variable. While this works, this isn’t an ideal choice, since globals hinder testability and flexibility. The second option is to have the presenting view controller maintain a reference to the coordinator. This will force a little complexity onto the presenting view controller, but will allow us to remove more complexity from all the view controllers that are managed by that coordinator. However, this relationship is fundamentally flawed. View controllers are usually “children” to coordinators, and when programming, children shouldn’t know who their parents are. I would liken this to a
UIView having a reference to a
UIViewController: it shouldn’t happen.
If you have a situation where you’ve decided that you absolutely must start with some child flow in your app, then you can make it work with one of the two methods above. However, if you have the power to start from the root, that’s my recommendation.
One other benefit to starting from the root is that the authentication flow is often close to the root of the app. Authentication is a great flow to isolate away into its own object, and a nice testbed for proving coordinators in your app.
Once you’ve moved the root view controller of the app to your
AppCoordinator, you can commit/pull request/code review/etc the code. Because every other view controller transition continues to work, the app will still be fully functional in this halfway state. At this point, working one-by-one, you can start to move more view controller transitions over to the coordinator. After each “flow step” is moved to your coordinator, you can commit or make a pull request, since the app will continue to work. Like the best refactorings, each of these steps are mostly just moving code around, sometimes creating new coordinators as needed.
Once all of your transitions have been moved over to coordinators, you can do further refactorings, like separating iPhone and iPad coordinators into individual objects (instead of one coordinator that switches on some state), making child flows reusable, and better dependency injection, all of which are enabled by your new architecture.