Fixing Sofa’s Tight-Coupling Problem with Coordinators

Sofa was suffering from a tight-coupling problem between view controllers. I discovered this while trying to refactor my massive controllers. I stumbled upon the coordinator pattern, which ended up being a great solution.

A quick note

I’m a beginner when it comes to iOS development, and the coordinator pattern is an advanced one for me. I struggled with things that more experience developers would find trivial. There were moments where I thought I may have bitten off more than I could chew, but in the end it all worked out. I ended up learning a lot and improving the quality of Sofa.


The Problem

For 2019, I’m working on some pretty large features for Sofa and thought I better clean things up before diving in. I was running into the issue of tight-coupling while refactoring my ViewControllers.

Up to this point, I’d been using segues to handle navigation flow. This wasn’t a problem until recently. I wanted to fix my massive view controller problem, but having tight coupling between my ViewControllers was making this incredibly difficult. Luckily, Paul Hudson wrote about the Coordinator pattern on Hacking With Swift. Perfect timing!

After a quick read, it looked like the Coordinator pattern would solve my tight-coupling issues. So I dove into implementing it.


The Outcome

The coordinator pattern has been successfully implemented and is live in Sofa 2.7.2. Woot!

This completely fixed the tight-coupling issue that was holding a lot of improvements back. While I’m really happy with the results, there were plenty of tricky details that I struggled with.


Tricky Details

Child Coordinators

For the coordinator pattern to work, you need to have a single MainCoordinator that everything flows through. To segment out your code you can create child coordinators that communicate back to the MainCoordinator.

I wasn’t initially sure when to make a child coordinator vs just adding it to MainCoordinator. The rule I ended up following was this:

If a view controller needs to present (not push) another view controller, it should be its own child coordinator.

This worked fairly well and forced me to not over-think things.

Delegation

Delegation was something that I understood at a high-level, but struggled to grasp the details of. Halfway through implementing coordinators, I realized, “Oh, this is just delegation.” It was a nice “ah-ha” moment, and ended up giving me a deeper understanding of how delegation works.

Protocols

Similar to delegation, protocols were another area of mystery for me. Having to create the Coordinator protocol, and implement it on multiple classes, enabled the concept to really sink in.

I ended up making one adjustment to the Coordinator protocol from the Hacking With Swift example by adding a stateMachine property to help manage state across the app.

protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] {get set}
    var navigationController: UINavigationController {get set}
    var stateMachine: SofaStateMachine {get set}

    func start()
}

Presenting Modal views

The main navigation flow for Sofa is handled by a subclassed navigation controller: FloatingActionNavigationController. This is so I can use the Floating Action Button (FAB - That little blue button that floats in the bottom right corner).

Pushing views with coordinators was no issue, but I was getting stuck on how to present views modally. Luckily Oli Pfeffer was able to explain how to resolve this.

Let’s look at the SettingsCoordinator as an example. This settings view controller is presented modally on top FloatingActionNavigationController.

First, we make it conform to the Coordinator protocol and initialize navigationController.

class SettingsCoordinator: NSObject, Coordinator {
    var childCoordinators = [Coordinator]()
    var navigationController: UINavigationController
    var stateMachine = SofaStateMachine.shared
    weak var parentCoordinator: MainCoordinator?

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
}

Next, we want to add a private var for the settings navigation controller. This will be used to push other views into this navigation controller.

private var settingsNavController: UINavigationController?

Finally, we put it all together in start()

func start() {
	//Instantiate the SettingsViewController
	let vc = SettingsViewController.instantiate()
	vc.coordinator = self

	//Create a new UINavigationController and set its root view
	//to the SettingsViewController we created above
	let nav = UINavigationController(rootViewController: vc)

	//Set the private settingsNavController to the newly created nav
	//This will be used to push other views into this UINavigationController
	settingsNavController = nav

	//Present the nav controller
	navigationController.present(nav, animated: true, completion: nil)
}

This may look simple, but in the moment I was incredibly confused 🤪

Removing views from memory

This is the most tedious and manual part of using coordinators. You need to make sure you’re removing the child coordinators you’ve created from memory. Since I had a few levels of child coordinators it took a lot of mental effort to implement and test. Overall, the effort was worth it for the final result.

Removing Unwinds

The amount of unwinds I was using prior to coordinators was tragic. SO. MANY. UNWINDS. In the end, most of the unwinds were resolved by using delegates instead. YAY for learning more about delegation!


Summary

I’m glad I invested the time to implement coordinators in Sofa. Overall, it took me about two months (6 hours per week) to complete. Sofa is now in a better position to handle more complex features, and it taught me more about key iOS concepts: delegation and protocols. Wins all around.


Resources That I Couldn’t Have Done This Without

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Poohbers.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.