Managing view state using Combine
In the previous post, we looked at handling model observations using Combine. Let’s take a look at how the Combine framework can help us drive view state from the model even when working with UIKit.
The difference here is that we’ll be looking at active state rather than one-time events. A good illustration of this would be a simple stopwatch app. Let’s begin by modelling state with an Enum:
‘Stopwatch’ will act as our model object. As state can change at any moment during our app’s lifecycle, we will want our UI to respond appropriately. Combine provides a special kind of Publisher called a Subject. Subjects allows us to publish values dynamically via their 'send' method.
CurrentValueSubject is an Apple-provided, concrete implementation of a Subject. We specialise the output of this Subject to be our State type, and also declare that it can never fail. We also assign it an initial value of inactive. This is where CurrentValueSubject and PassthroughSubject differ. In fact, Apple’s documentation provide the clearest explanation:
“Unlike PassthroughSubject, CurrentValueSubject maintains a buffer of the most recently published element.”
This works nicely for our stopwatch app; which has to be in 1 of the 3 states we’ve declared in our Enum above. When we change the state (say, from inactive to active) CurrentValueSubject holds this in its ‘value’ property. We can subscribe to CurrentValueSubject in the same way as PassthroughSubject. Using the sink method:
To actually trigger a state change, a user may start or stop the stopwatch. However, we don’t want our StopwatchViewController to make the selection of which state to change to. Instead, our model is in charge of reading current state and transitioning to a different one. Our Stopwatch class (our model) can expose the following method:
Great. The state change logic is nicely contained within our model. No need for StopwatchViewController to know anything about it. Instead, the view controller should only ‘read’ state when it receives published values via the subscription it holds.
However, despite our best efforts separate concerns here, StopwatchViewController would still be able to manipulate state by accessing the send method of our CurrentValueSubject. Whilst this can be avoided with a bit programmer care, it would be nice to design this out via our model’s API.
Thankfully, we can use an alternative approach with another tool in the Combine toolkit: the @Published property wrapper.
@Published allows us to make any of our properties publishable. With this, we can now achieve our goals of read-only (or rather, subscribe-only) access from outside of the model.
The code on the StopwatchViewController side looks remarkably similar to before, except for one extra character. The ‘$’ prefix is @Published’s way of providing access to the publisher of our state property (rather than the raw state property itself).
Finishing the app…
At this point we could continue to develop our stopwatch app further but, rather than detail all of this here, you can grab the full source code for the completed app below. Here, you’ll be able to see how a single @Published property on the model drives everything (including the rather essential feature of a stopwatch app - the actual time value!)
Here, we saw two ways to publish state changes within our app using Combine. We compared both CurrentValueSubject and @Published, making a selection based on the goals of our app. In future posts, we’ll dive into some of Combine’s operators which can be used to transform and format model data for use within UIs.