Refactoring imperative code using Combine
A great feature of Apple’s Combine framework is that it can be adopted incrementally. Many of us are working on imperative codebases. Even with the luxury of being able to adopt iOS13 frameworks in production apps, it’s unlikely that we’d want to undertake a large scale refactor in one go.
In my own apps, I’ve begun using Combine as a tool for simplifying model observations. In this post, I’ll focus on a quick win when updating a Swift codebase.
The old way
This has always been an aspect of iOS programming that could be achieved multiple ways. NotificationCenter, KVO, callback closures or even a delegate-style protocol. There’s no correct answer and choice typically comes down to developer preference.
Using Combine, we get to reassess the nature of model observations and hopefully understand them better. Our views often need to observe one-time messages from the model to keep the UI in sync with the underlying data. Using NotificationCenter, deleting an item from a simple list may look like this:
Despite their versatility, notifications come with some drawbacks. Accessing a value in the notification's userInfo dictionary using a stringly-typed key and then optionally typecasting doesn’t feel very Swift-y. Also, without careful management, it’s possible for these notifications (i.e. implementation details) to leak into the global namespace. Let’s see how Combine can help here.
The Combine way
At the call side this reads nicely. Even though it’s not dramatically less code, it’s clear what's happening. On a change to the model, we attach ourselves as a subscriber to this change using Combine's 'sink' method. The closure is called every time a change happens. Let's take a look at the point of declaration in the model of 'onChange'.
Here, we have a single property with read access that allows us to publish changes in the model. PassthroughSubject is a concrete type provided by the Combine framework which we can use to publish a value whenever it changes. PassthroughSubject is appropriate for events such as CRUD operations as there is no need to maintain a ‘current value’. We are representing an event here, rather than state or a value.
Let’s look at the implementation of the ‘delete’ method.
The last line here is how we publish our model updates. Using 'send' notifies all subscribers of this change. This calls the closure of the ‘sink’ method in our view controller.
You will have seen I’ve made use of a Swift type to encapsulate the model changes. The enum Model.Changes takes care of describing the finite number of possible changes to observe. And specialising the generic class, PassthroughSubject, allows us to maintain type safety at the call side (unlike using the userInfo dictionary of Notification).
Here we saw a very simple example of how we can adopt Combine in our imperative code bases. PassthroughSubject provides an easy, locally-scoped way to broadcast events where previously we might have used NotificationCenter.
In my next post, I’ll show you how CurrentValueSubject can be used to manage state for a specific feature of an app.