Making Talko Reactive
When Cocoa Alone Doesn’t Cut It
Apple’s Cocoa Touch framework enabled the app store revolution, but it has not kept pace with the sophistication of many modern applications. To deliver a dynamic user experience in Talko, we used ReactiveCocoa — a functional reactive programming framework for Objective-C.
The Talko iOS application allows teams to communicate in real-time and anytime (i.e. synchronously or asynchronously). It is critical that the user interface feel alive and aid the user’s perception of being co-present with others when multiple people are LIVE together. To do this, Talko needs to constantly react to actions of the current user, actions of other users received over the network, changes in network conditions, changes in the state of the application itself (i.e. foregrounded, backgrounded), etc.
Sometimes the reaction is a change in the user interface such as rendering a new photo or an unread bar. Other times, the reaction may involve changes to the internal operations of the networking stack such as switching to a WiFi network from the cell network when appropriate.
Here is an example of dynamic UI when viewing a list of calls while another user makes changes. Notice that UI automatically updates when the other user enters and makes additions to the “Planning for Next Week” call. The other user’s icon shows up indicating that they are live in the call at the moment. The unread bar is rendered as the other user adds text and a photo to the call. Finally, the other user’s icon goes away when they exit the call.
Here is another example from within the call UI. This animation shows how the call header changes from a purely textual mode to one that to render user icons as soon as a second person enters the call. You can then see Howard’s icon goes from gray to color when he enters the call, and then see a blue activity ring around his icon when he begins typing.
Early in Talko’s development, it became clear that varying ad-hoc approaches to these widespread dynamic requirements would not be a good idea. We needed a robust and consistent design pattern. At around that same time, ReactiveCocoa (RAC) was gaining traction in the Cocoa community. ReactiveCocoa is an Objective-C Functional Reactive Programming framework based on Erik Meijer’s Rx — Reactive Extensions for .NET. Having had prior experience with Rx, ReactiveCocoa looked promising to me.
What is Functional Reactive Programming?
The easiest way to think about the reactive programming is to think about a a spreadsheet. Imagine cell A1 contains the formula “= B1 + C1”. We know that whenever the value of B1 or C1 changes, A1 will automatically be updated. Let’s also say that C1 has the formula “=D1 + 10". We can think about the data flowing through the spreadsheet. D1 gets updated, causing C1 to gain a new value which, in turn, causes A1 to gain a new value. The programmer declares the relationship between cells allowing them to automatically react to new data. With A1 depending on C1, we also see that declarations can be composed together to form more complex relationships.
A traditional imperative approach might look something like the code on the left. In this case, the coder has to keep track of all of the state and be sure to call updateC1() and updateA1() at all of the right times and in the right order. The difference in approach is obvious.
In functional programming, as in the spreadsheet analogy, functions simply provide outputs when given inputs — maintaining no state and inducing no side effects. FRP also uses higher order functions such as map, reduce and filter from the functional world.
Although Cocoa Touch does have some building blocks of reactive programming in the form of KVO, NSNotificationCenter and delegate interfaces, they are provided in the form of inconsistent, bothersome and syntactically clumsy APIs. ReactiveCocoa brings to the table, among other things, a consistent, concise, and composable abstraction of the reactive programming concept in the form of signals.
A signal can be thought of like a pipe through which values flow. Continuing with the spreadsheet example, we could declare the signals for C1 and A1 as shown to the left. The signal for c1 is composed with d1. Whenever d1 sends a value, c1 transforms or maps that value by adding 10 to it and sending that value on its own signal. The signal for a1 is another composition that combines two signal values by adding them together and returning that value. These signals obey the functional properties described above. They maintain no state. They induce no side effects. They simply turn inputs into outputs.
Of course, a program with no side effects is pretty boring. At some point, values flowing through a signal must have some impact on state or the user interface. This is typically done in ReactiveCocoa at the end of a signal chain by subscribing to the signal and performing some logic when each value is received. The binding code to the left is a trivial, but common, example. It subscribes to the values flowing through self.imageSignal and assigns them to the image in a UIImageView.
Signal values can represent any type concept. A signal could send values representing the fact that the user tapped a button, or a value representing the fact that a network connection was gained. Encoding all of these concepts in the form of signals allows declaration of signal chains for many aspects of the product using the same fundamental compositional operations.
Talko Signal Examples
Call Header View Model
Let’s examine the signal for the blue “typing” ring in the call header view we looked at earlier. The view model code below shows how we declare the signal that makes this work. The signal is a composition of other signals — isTyping, isTalking and micOpen. Every time any one of those underlying signals sends a new value, the code decides which image should be rendered and sends that image as its value. The call header then binds this signal to a UIImageView’s image property.
Remember that the view isn’t polling for new values — the binding causes new values sent through the signal to be automatically set into the view. Also notice how the code that reacts to a user typing is cleanly separated from the code that determines a user is typing — enabling clean unit testing.
Non-User Interface Signals
Signals can be just as useful for non-user interface code. This example involves Talko’s offline support. If a user has any content that was created while offline, it must be sent when the user regains a connection to our service. This signal declares that whenever a new session is created, and syncing from the service to the client has completed, it should wait for two seconds and then send any pending offline data.
Asynchronous Serialized Processes
While not specifically functional, we also often use ReactiveCocoa signals as a way to chain asynchronous operations. The following signal represents four steps required to invite a set of Talko users and non-user addresses to a call. Notice how each step depends on a value provided by the previous signal.
You may be thinking that in these examples the extra level of abstraction doesn’t seem worth it. For trivial examples, it may not be. However, as the complexity of a problem grows, so too does the complexity of imperative solutions. This leads to code that is hard to debug, test and extend. Imperative code can hit a complexity wall. This isn’t the case with reactive code.
Reactive code scales to solve complex problems while remaining well factored, pliable, extensible, testable and maintainable.
Making the Transition
Adopting ReactiveCocoa requires a somewhat fundamental change in the way that you model systems. Thinking about solutions in the context of data flow and composable building blocks can feel unnatural at first. It’s important to take a gradual and measured approach and build up your understanding as you go.
I suggest converting one or two uses of KVO over to RACSignal’s as a first step. Native KVO code that looks like the following…
…can be done with the following ReactiveCocoa code.
The “RACObserve()” macro returns a signal for a KVO compliant property. The code then binds the values from that signal the label’s text property. This code has much better locality compared to the KVO code which spreads the concept of observing “callLabel” across three methods.
Once you’re comfortable with that KVO -> RACSignal conversion, it’s best to learn a few more of the common basic signal operations (defined in RACSignal+Operations.h) such as concat:, merge:, throttle:, take: and interval:. As your understanding grows, you’ll see more conversion opportunities in your code.
An important milestone will be when you include a RACSignal method or property in a class’ public interface. If you’re using the MVVM pattern in your application, pick a single, simple view/controller + view model pair for conversion. Strive to have the view/controller as simple and declarative as possible binding to RACSignal’s provided by the view model API.
Finally, the next level of complexity are operations that deal with signals whose values are also signals such as flatten:, switchToLatest:. Although these are quite powerful, it’s very important to have a firm grasp of the basic operations before moving on to these.
Reactive programming, like any powerful abstraction, comes with a cost. In many scenarios, the extra cost is immaterial. However there are cases in which great care must be taken and pragmatic compromise may be needed. Two general cases are worth mentioning specifically.
When the number of values sent is a very large number
At one time Talko consumed inbound TCP messages as values sent on a signal. There are cases where the Talko client can receive many, many messages in a very short amount of time — for example, when an existing user with a lot of data installs on a new device. In this full sync scenario thousands of messages can be received and delivered as signal values to roughly twenty interested subscribers. In this case, the incremental cost of consuming the messages via signal subscription vs the simpler (but less composable) NSNotificationCenter approach was extremely high and caused us to forgo RAC in this scenario.
When UITableViewCell’s and UICollectionViewCell’s Use Signals
This use case is particularly sensitive since with a simple swipe of the finger, the user could easily cause hundreds of cells to be created in a short amount of time. Keeping a frame rate of 60fps means that each execution of the main thread’s runloop has ~17ms total. Depending on the hardware, we’ve found that a single RACObserve can take roughly 1.5ms. The results of that math are obvious. We’ve taken steps to keep our scrolling frame rate up such as:
- Defer as much signal creation/subscription as possible to a background thread.
- Even though it would be natural in reactive terms, don’t observe a single property on yourself that might change frequently — such as self.viewModel — to simply turn around and call another method. When scrolling a table or collection view, a cell will receive a new viewModel from the signal many times. In this case, we make the compromise with imperative code in this case as shown below.
- Because signal creation and subscription can be relatively expensive, use an immediate imperative + later reactive approach in cells. This requires refactoring code so that at view model assignment time, a cell renders its content by calling view model properties immediately and imperatively. If the cell remains in use for a small number of seconds, only then are the dynamic bindings created.
In building the Talko iOS client, we found:
- Native Cocoa Touch design patterns are not sufficient for building dynamic, reactive iOS applications.
- ReactiveCocoa is an incredibly powerful framework that addresses Cocoa Touch’s shortcomings and makes dealing with complex requirements much easier.
- ReactiveCocoa has a learning curve. Take conversion slowly and one step at a time.
- ReactiveCocoa costs, but it’s usually well worth it. Be particularly careful when the number of subscriptions and values is high.