A Case of Not Really Using Reactive Cocoa
I have been using Functional Reactive Programming (FRP) with Reactive Cocoa (and lately Reactive Swift) in production projects for almost two years and have seen a few people adopt it. Throughout the process, we learned not only how to use FRP but also how not to use it. In this article, I will reflect on some typical novice missteps I made myself, so that someone coming from a background of a traditional imperative or mixed-flavour language like Objective-C, Java or C# could perhaps learn from my mistakes.
We will consider a problem and three different solutions to it:
- naïve (and buggy) imperative solution,
- correct imperative, and
- correct FRP-style solution
Hopefully at the end, you will get a sense of how FRP can be beneficial to solving some of your problems related to UI and asynchronous programming.
This article assumes familiarity with basics of Reactive Swift (in particular the notions of Signals and SignalProducers). You can quickly catch up with those using ReactiveSwift README documentation.
Problem
Suppose we develop a shopping app with a view controller that needs to display a list of product descriptions available from a remote API. There’s a textual description and an image URL. The image will have to be downloaded using a network request. We will focus on writing code for the table view cell in an MVVM fashion assuming the network request is abstracted using a Reactive Swift’s SignalProducer
.
Naïve solution
At first glance, it might look as though the implementation of ProductTableViewCell does what it’s supposed to:
- it takes a viewModel
- it sets the values of text fields
- it downloads an image and configures the image view with the result.
Our job is done, right? — No, we’ve got bugs! The first problem one will notice about how this code works will perhaps be that sometimes wrong images show up against product descriptions during scrolling. Why is that?
Well, as the user scrolls the list of products ProductTableViewCell
objects get reused, i.e. the same cell will represent different Product
objects throughout its lifetime. In other words, the value of viewModel
property will actually change (we knew it would from the beginning, didn't we?). Now let's consider a specific timing of events where Cell 1 first displays product a, then while the user scrolls, product a goes off screen and Cell 1 is re-used to display product b, then the user stops scrolling (we use this assumption for certainty with regard to values). Suppose that scrolling and cell re-use have occurred before the download of image a has been completed, and image b download will take some time. The download signal for a will then complete when the cell is already used to display product b and there will be a time span when the image for product b is not yet ready to display — this is where we are going to see the image for product a against the description of product b in the list (amd misattributing brand names to other brands' looks is the last thing that we want in our shopping app!) Here’s a little sequence diagram to illustrate the timing of the events described above.
Turns out it’s not the only issue we can get with this code. Consider the following sequence diagram where the download happens faster than scrolling takes Cell 1 off the screen. We will observe image misattribution again when the image for product a in the reused cell is replaced by the correct image of product b.
Let’s just fix it!
We are strong! We can fix all that in no time! (Actually, it will take some time, but shush!) We will terminate any previous subscription to download signals when the cell receives a new value of viewModel
. This way we will address the issue with Sequence 1. Then with every new viewModel
value we will replace the image in the cell with a placeholder (in our case, just nil
) in order to prevent the mismatch between the displayed textual content and the image as described in Sequence 2. Sounds like a plan?
With these fixes in place the cell will display only the image corresponding to the assigned product or a placeholder. We are finally all set, aren’t we?.. — Looking at this code, let’s ask ourselves, why have we even used Reactive Cocoa here? What advantages have we reaped from learning and using a new library?
- Mutable properties! — Not really, we could have used a built-in Swift’s didSet observer here.
- Signal producers encapsulate state of asynchronous operations here! — Yes, but so do
Operation
objects from Foundation that we all know. We could have defined aDownloadImageOperation
with a dependentBlockOperation
and accomplish the same without even learning Reactive Cocoa.
Could we do better? — Yes. What happened here was, we used FRP the way we would use our usual imperative tools without considering what the approach had to offer for both UI and asynchronous programming problems that we dealt with here.
FRP Solution
Two great tools that Functional Reactive Programming leaves at our disposal (and was created to facilitate) are:
- Bindings for UI programming
- Combining signals with functional style operators (such as map, filter and flatten) and naturally managing lifetime of the resulting signals.
Here’s how we can apply those to our problem (for clarity, we’ll only focus on the implementation of awakeFromNib
(the rest of the original code won't change):
Let’s clarify exactly what we are doing here.
<~
is a binding operator that acts on values implementingBindingTargetProtocol
(left-hand side) andBindingSourceProtocol
(right-hand side). Applying the operator to the values subscribes the target to values emitted by the source for the lifetime specified by the target (in our case, it's the lifetime ofUILabel
s andUIImageView
), i.e. the subscription is guaranteed to terminate with the specified life time.- Note how in lines 3 and 4 we map properties directly instead of mapping signal producers. This feature of properties only works for this purpose starting from Reactive Swift 1: although it was technically possible to map a property in Reactive Cocoa 4, the lifetime of the temporary property objects would only allow the binding to pick up a current value from the source property.
- In the last line of
awakeFromNib
function we useflatMap(.latest)
operator to transform a signal producer of view models into a signal producer of images. How is it better thanstartWithValues
call that we used in our first attempt to approach the problem? — Under the hood, it does exactly what our fixed solution does: it disposes of any subscription to download signal that does not correspond to the latestviewModel
value, the subscription state is encapsulated in the implementation of the operator. This handles the edge case described in Sequence 1. - We prepend each new image download signal with a
nil
value in order to hide any image that was previously in the view and avoid the situation described in Sequence 2. One could think of this value as a placeholder image that arrives immediately upon signal start.
Thus we have derived a correct solution to our problem in just a few lines of declarative code without worrying about particular sequences of events in the app — we specified what we needed to get rather than how to get it step-by-step — using bindings and functional style operators. This is what FRP is designed to facilitate.