UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Using closures with the coordinator pattern

Fight against massive view controllers with coordinators on iOS.

Paul Hudson       @twostraws

I talk about coordinators a lot, but there are two questions I get asked a lot:

  • How is it different from the delegate pattern?
  • Why not use closures?

I figured it was time I wrote down permanent answers to these questions so I had something to point folks to.

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Coordinators: the two-minute recap

Before we dive in, I want to briefly outline what coordinators are for those of you who haven’t heard of them before. Coordinators originated with Soroush Khanlou way back in his 2015 article, then were expanded upon in several follow up articles. I’ve also written about the pattern, but I highly recommend you read Soroush’s work.

At their core, coordinators allow us to take navigation logic out of our view controllers, so they have no idea what comes next in our app flow or even that an app flow exists. This means your view controller no longer needs to know about, create, configure, then display an entirely different view controller.

So, instead of writing this sort of code:

navigationController?.pushViewController(detailVC, animated: true)

We instead add a coordinator property to our view controller, ideally using a protocol so we have more flexibility, then call methods on it when something happens:

coordinator?.purchase(product)

Using this approach, the coordinator is responsible for deciding what should happen next: it might take one path for iPhone and another for iPad, it might do different things based on which A/B test the user is in, and so on.

Why not just use delegates?

The delegate pattern is hugely important in the world of Apple development. In fact, in my book Swift Design Patterns delegation is literally the first pattern that’s covered – it’s extremely useful for breaking out functionality, and is a central technique for tackling massive view controllers.

But here’s the trick: coordinators are just specialized delegates. Take a look at this code again:

coordinator?.purchase(product)

If you want to call it a delegate instead, our code magically transforms into this:

delegate?.purchase(product)

All that’s changed is the name of the property being used – the technique of isolating then carving off navigation code hasn’t changed. That being said, using something specific like coordinator allows you to be more fine-grained in your delegation – you might have several different delegates all working together to support a single object, because there’s no point trading massive view controllers for massive delegates.

Using closures for iOS navigation

When a view controller has only a handful of coordinator callbacks, I often use closure injection rather than sending in a coordinator instance.

So, rather than having a property like this:

weak var coordinator: MainCoordinator?

I would instead have properties like this:

var readAction: ((Article) -> Void)?
var buyAction: ((Product) -> Void)?

That allows to inject callbacks into a view controller rather than a specific coordinator. This has the effect of creating even more loose coupling than having a coordinator object or protocol, allowing us to replace coordinators with something else entirely if we wanted.

From the view controller end, we can trigger those closures like this:

func read() {
    readAction?(someArticle)
}

From the coordinator end – because coordinators still exist, and still do all the job of controlling navigation – we would write something like this:

detailVC.readAction = { [weak self] in
    self?.read(article: $0)
}

Note: Make sure you create a new closure and use weak self to avoid the possibility of a retain cycle.

This technique of closure injection is nothing new – in fact, Apple even mentioned it in their WWDC 2016 presentation, Improving Existing Apps with Modern Best Practices. However, I think it has its limits: if you try to pass in more than a couple of closures it gets messy, and you should consider using a coordinator object or protocol instead.

Bonus alternative: protocol composition

There’s one other technique I’d like to mention briefly, because it’s one I find extremely useful.

When making a small app with coordinators, my view controllers have a concrete coordinator type as their property rather than a protocol, like this:

weak var coordinator: MainCoordinator?

When making more advanced apps I’ll add in coordinator protocols that allow me to change which concrete coordinator implementation is being used without affecting anything else, like this:

weak var coordinator: Purchasing?

Coordinator protocols work great for medium-sized apps, but for larger apps I switch over to using protocol composition, like this:

var coordinator: (AnswerHandling & AlertHandling)?

That way I get the benefits of being able to switch concrete implementations, but I also get to be more flexible in breaking up functionality.

Which works best?

Regardless of whether you choose concrete coordinators, protocols, protocol composition, or closure injection, the point is that you’re burdening your view controllers with one less responsibility. All four of those techniques are useful at different points of app development, so it’s worth trying them all out to see which one works best for your team.

Alternatively, if you have a different way of working with coordinators I’d love to hear more – get in touch on Twitter at @twostraws!

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.