This is the eighth post in my series on MVVM with ReactiveCocoa 3/4 in Swift.
In an MVVM architecture, it makes a lot of sense to expose view model properties in such a way that code interacting with your view model, typically the view layer, can observe their changes. In the early days of ReactiveCocoa (RAC), this often meant using regular NSString*
or BOOL
properties and turning them into signals in the view’s bindViewModel
method, typically through a RACObserve
macro.
Since the release of ReactiveCocoa 3.0 with its native Swift API, this approach no longer works, at least without sacrificing type safety. Instead, RAC comes with several types that we can use to publish our app’s internal state to the view layer, most notably Signal
and MutableProperty
.
The signal represents a stream that consumers can subscribe to without creating side effects. Imagine somebody tuning into an FM radio broadcast. The radio station, unlike an internet streaming provider, won’t know that this is happening. It just merrily sends out its content, and this is essentially what Signal
does too. (A SignalProducer
, by contrast, also performs side effects when creating its signals.)
Mutable properties are basically wrappers around the regular properties we all know. They hold the value itself, but also give us a signal or signal producer to subscribe to and receive updates on its value changing over time. The difference is that the signal will only send value changes that happen after subscribing, whereas the signal producer creates us a signal that sends the current value immediately, followed by all changes later on.
But hold on – didn’t I just mention that signal producers always perform a side effect when subscribed to? In fact, this puzzled me for a long time after first encountering RAC’s mutable properties, until I learned that the signal producer’s only side effect is the subscription itself. It’s a bit of a trick RAC’s creators used to give us a value immediately after subscription. ☺️
An example
As usual, we’ll take a look at the SwiftGoal example app to see how we can use the above types in a real project. This time we’ll focus on the “Outputs” section of ManagePlayersViewModel
, the class that backs a screen where the user can add players and select them for a match.
// ManagePlayersViewModel.swift
// Outputs
let title: String
let contentChangesSignal: Signal<PlayerChangeset, NoError>
let isLoading: MutableProperty<Bool>
let alertMessageSignal: Signal<String, NoError>
let selectedPlayers: MutableProperty<Set<Player>>
let inputIsValid = MutableProperty(false)
As you can see, we’re essentially dealing with three kinds of properties here:
- The view model’s title never changes and therefore modeled as a
String
. “Binding” to this value means simply assigning it to the view’stitle
property. - Content changes and alert messages are exposed to the view as signals carrying two different types: Each content change is represented by a
PlayerChangeset
, whereas error messages are modeled as plain vanilla strings. - The remaining three are mutable properties wrapping a boolean flag that tells whether content is loading, a set of
Player
models selected by the user, and another, initiallyfalse
flag for input validation when creating a new player.
If you’re curious how other classes (mostly ManagePlayersViewController
) bind to these exposed properties, check out the code that does exactly that.
Signal or mutable property?
Looking at the above outputs, you may notice that there is a qualitative difference between the three approaches. Consider the way their value behaves over time. The title is easy – it never changes. But whether our data is loading, what players are selected, or whether the player form input is valid – those are all examples of continuous data, which can be queried anytime. We could ask our view model at any random moment: “What players are currently selected?” and it would always have an answer for us.
The content changes and error messages, on the other hand, are discrete data. They represent singular events that happen, get noticed and handled by the view, but cannot be returned upon request. Asking the view model “What is your current content change?” doesn’t make any sense. (And because SwiftGoal presents errors as modal alerts to the user, modeling error messages as continuous values didn’t make much sense to me either. In an app where errors stay on screen as long as the problem persists, I would have picked a mutable property instead.)
Conclusion
As a guideline, ask yourself whether you’re dealing with continuous or discrete data before deciding on a type for your view model properties.
For continuous data, mutable properties work better. This is especially true if you’re moving in the borderlands between reactive and imperative programming, such as when implementing UITableViewDataSource
. In those cases, being able to ask the mutable property for its value whenever you need it is a good thing. I’m actually using this technique in the view model presented earlier.
For discrete events, consider using a signal. It doesn’t hold its value, so you can’t be tempted to mess around with it in non-reactive ways. Just react to whatever it sends you, and be grateful for every piece of state you don’t have to manage yourself. 😇