An Introduction to ReactiveSwift
Hi, I’m Patrick — an iOS Engineer at Mercari. Here at Mercari, our client engineering teams use the reactive programming paradigm. In recent years, this paradigm has grown and become more popular within the development community. In this article, I’d like to provide a brief intro to reactive programming using ReactiveSwift. To demonstrate these concepts, I’ve provided a sample project for you to reference alongside this post; it simply shows a random color when a button is pressed, but it illustrates how reactive programming can be used. Please note that this article assumes basic proficiency in Swift.
At Mercari, and in this sample project, we use Model-View-ViewModel (MVVM) architecture along with ReactiveSwift. The ReactiveSwift library allows us to more easily apply the reactive programming paradigm. At the heart of reactive programming is the concept of a stream — a stream simply sends values and, when we observe them, we can take action. In ReactiveSwift, this concept of a stream is represented by the Signal
class. We can model user interactions as signals. This pairs well with our architecture of choice, MVVM.
The ViewModel
The ViewModel is one of the most integral parts of MVVM. Any logic that must be performed should live inside the ViewModel. This clearly separates responsibilities throughout an application: ViewModels handle the logic, while views and ViewControllers are updated based on the output of this logic. It’s easy to represent this pattern as inputs and outputs within the ViewModel layer. Here is how this looks at a high level:
Inputs
You can think of inputs as actions taken, whether performed by the ViewController itself (viewDidLoad
) or the user (taps and swipes); we want to act on them to perform some work. In the sample project, we can see how input is captured. As seen below, the RandomColorViewModelInputs
protocol declares functions that will be called when certain action is taken:
protocol RandomColorViewModelInputs {
func viewDidLoad()
func newColorButtonTapped()
}
For example, viewDidLoad()
is called when the viewDidLoad
method fires inside our ViewController. We can make these functions reactive inside the implementation using ReactiveSwift’s .pipe()
. This allows us to create a Signal
that we can send input through and observe the resulting output. Here we can see the definition of viewDidLoadIO
(our pipe, IO
for input/output) and the viewDidLoad()
(from our protocol) that provides the input:
private let viewDidLoadIO = Signal<Void, NoError>.pipe()
func viewDidLoad() {
viewDidLoadIO.input.send(value: ())
}
viewDidLoadIO
is a signal that makes use of .pipe()
. It sends a value of Void
and has NoError
as the associated error type (essentially meaning it will never error). Inside the function viewDidLoad()
we can see our Void
input is being sent through viewDidLoadIO
. Here is how the overall flow through viewDidLoadIO
looks:
Outputs
Outputs are the result of the actions taken. For example, if viewDidLoad
is called, we want to generate a random color and display it to the user. There is only one output defined in this ViewModel. Inside our output protocol we can see the declaration of a single signal, displayModelSignal
. It sends a struct that contains information about how our view should look:
Protocol RandomColorViewModelOutputs {
var displayModelSignal = Signal<RandomColorDisplayModel..> { get }
}
When our view first loads, we want to configure it; that’s where the viewDidLoadIO
from above comes in. viewDidLoadIO.output
is just a signal — it’s the other end of the input
pipe. We can transform this signal to create thedisplayModelSignal
, that when observed will provide updates to our UI. Don’t let this intimidate you; we’ll break it down step-by-step in the next section:
var displayModelSignal: Signal<RandomColorDisplayModel, NoError> {
return Signal
.merge(
viewDidLoadIO.output,
newColorButtonTappedIO.output
)
.map { RandomColorDisplayModel() }
}
The important thing to note is that by using the viewDidLoadIO.output
signal, we can observe a value of Void
when viewDidLoad()
fires. A value of Void
is not very helpful, but in ReactiveSwift we can use operators to transform the values sent by signals. In this case, Void
would be mapped to a RandomColorDisplayModel
struct.
Transformation
In the example above, we can see the computed property displayModelSignal
— this is just the implementation of the signal declared in our RandomColorViewModelOutputs
protocol. The returned signal is composed by two operators: merge
and map
. It looks a bit complicated, but let’s break it down:
Signal
.merge(
viewDidLoadIO.output,
newColorButtonTappedIO.output
)
Signal.merge()
takes whatever signals we provide and merges them into a single signal. This new signal will send Void
when viewDidLoadIO.output
or newColorButtonTappedIO.output
send Void
. This also means that, within the merge declaration, the values produced by our signals must be the same. This brings us to the map
operator:
.map { RandomColorDisplayModel() }
Just like with a collection, map
allows us to transform each value sent by a signal. In this case, it’s fairly simple — when our merged signal sends a value (Void
) we return a newly initialized RandomColorDisplayModel
. More complicated examples may call for transformation based on what values are sent.
Observing Change
Now we have inputs and outputs in our ViewModel. We just need to send input and observe output inside the ViewController, which can been seen insideRandomColorViewController
. There is a declaration of the viewModel
at the top (using our RandomColorViewModel
):
final class RandomColorViewController: UIViewController {
let viewModel = RandomColorViewModel()
}
Using this ViewModel, we can capture inputs. In order to create outputs, there must be inputs. Therefore, when action is taken, we want to call the corresponding input method. Inside viewDidLoad
, we call the viewModel.input.viewDidLoad()
method. As discussed above, this will send a value of typeVoid
through our viewDidLoadIO
pipe:
override func viewDidLoad() {
super.viewDidLoad() bindViewModel()
viewModel.inputs.viewDidLoad()
}
Now all we have to do is observe these changes, this where bindViewModel()
in the code snippet above comes in. This method allows us to configure the observation of output and the subsequent actions taken based on this output. All output signals from the ViewModel can be observed using .observeValues
. This allows us to create a closure that has parameters of the values being sent inside the signal. The closure will take these values as parameters and perform updates:
private func bindViewModel() {
viewModel.outputs.displayModelSignal.observeValues { [weak self] in
self?.colorDescriptionLabel.text = $0.description
self?.colorView.backgroundColor = $0.color
}
}
Since work on self
is being done in this closure, we should weakify self — hence the [weak self]
. Also note that $0
is an anonymous closure argument — in this case it represents the RandomColorDisplayModel
struct. With these details in mind, the body of our closure simply takes the struct sent by the signal and applies it to the ViewController. The background color and label text are set using the values on the RandomColorDisplayModel
. This process is performed every time a value is sent through our displayModelSignal
.
Conclusion
Although this is a fairly simple example, it demonstrates an application of reactive programming. It applies a strongly defined and repeatable pattern. The UI updates are cleanly modeled and handled in a central location. Of course, as the UI grows in complexity, it will make sense to break the displayModelSignal
into smaller, more manageable parts. I hope this article has inspired you to give it a try. Thanks for reading!