Reactive Programming with Swift

Collective Health
Building Better Healthcare
6 min readAug 8, 2017

--

by Abhishek Savant

Nowadays, reactive programming with RxSwift has become the new buzz word in Swift programming. The first definition that came up in Google was: “Reactive programming is programming with asynchronous data streams.” What does that really mean?

In this article, we are going to learn the basic principles of reactive programming and ways in which RxSwift — a Swift library for reactive programming — is implemented in Collective Health’s iOS app. For me, the best way to understand a programming construct and the motivation behind it is to learn about the problems it solves. First, let’s understand the challenges developers face with asynchronous programming.

Asynchronous nested callbacks

All our applications need some form of asynchronous programming, which traditionally means adding heavily nested callbacks. Nested callbacks are easy to implement but difficult to understand, read, and debug. This complexity only grows when you need to synchronize multiple asynchronous tasks. The solutions for synchronization are locks or dispatch queues but “they” are not ideal and can lead to debugging issues if not written carefully. More often than not, easy is interchangeably used with simple but in many programming constructs simple is NOT easy. In this case, asynchronous callbacks can add complexity to your software.

Nested callbacks are easy to implement but difficult to understand, read, and debug.

Let’s look at a nested callback example. In the code snippet below, an async task is created to download image from url, which on success is set on UIImageView on the main thread.

Figure 1 — Nested code is difficult to understand, read, and debug

In the above code snippet, we download the image on a background thread, and then update the imageView content by animating its alpha on the main thread. In iOS, the main thread renders UI and handles events. Grand central dispatch (GCD) — high level abstraction of threads in Swift — enables us to execute blocks asynchronously on dispatch queues. Blocks are simple to implement, as the task becomes complicated we end up writing nested blocks. Javascript developers coined the term “callback hell”. Javascript community resolved this problem by making promises and async functions part of ES6 Javascript standards. Similarly, the Swift community adopted Functional Reactive Programming, with use of libraries such as RxSwift.

What is Reactive Programming?

Terms like “reactive” and “propagation of change or events” do not provide a clear definition of reactive programming. Reactive programming is a way of responding to asynchronous streams of data.

Figure 2 — Propagation of change or events

In simple words, streams of data emitted by one component are captured by another that subscribe for data changes. In Figure 2, streams of data emitted by the observables are processed by an operation, which in-turn emits the desired output along with a completion signal.

Figure 3 — Propagation of error

In Figure 3, the error thrown by the operation is emitted in the onError() callback without triggering the completion signal. The concept of data streams is not something new; tap or click events are asynchronous event streams on which we can observe on and perform actions.

In reactive programming, data streams are captured asynchronously by a defining callback functions that both execute when a value or error is emitted and a completion signal is posted.

RxSwift consist of three parts:

  1. Observable: Observables emit streams of data based on certain events. Data streams are first class citizens. Therefore, data manipulation is simplified with functions such as map, flat map, reduce, filter, concat, merge, etc.
  2. Observers: Observers subscribe to state changes in data streams. It registers to Observables that emit data streams with the help of subscribeOn() method. The observer receives the streams of data in the onNext() callback and errors are recorded in the onError() callback. Finally, the onComplete() callback is triggered; informing the subscriber all observables operations are done. This only happens after the entire data stream is emitted successfully.
  3. Schedulers: Schedulers are the thread management component of RxSwift. The observeOn() operator specifies the thread on which the observer receives events, whereas subscribeOn() specifies the thread on which work is executed. ObserveOn operator can be called multiple times per stream of data to move between threads but SubscribeOn() can only be called once. By default units of work are scheduled on the CurrentThreadScheduler — schedules on current thread.

Now, let’s refactor the code snippet from Figure 1 with RxSwift library. In the code snippet below, the RxSwift’s URLSession downloads image from given url, which on subscription executes onNext(), onError(), and onCompleted callbacks.

Figure 4 — Refactored using RxSwift

As events are single threaded, the onNext(), OnError(), and OnCompleted() callbacks handle all the UI updates on the main thread in sequence. The code exhibits the following characteristics:

  1. Simple threading
  2. Readable and easy to understand
  3. Strongly typed callbacks (explicit)

How do we use RxSwift at Collective Health?

As our team became more familiar with RxSwift, we decided to refactor our networking layer by utilize reactive programming patterns. Collective Health’s iOS app has a feature called “Get Care” that allows our members to find in-network doctors near them. In order to enable the search, we gave our members the ability to specify a location using a typeahead that leverages iOS’s MKLocalSearchCompletion feature.

Let’s dive into the search typeahead code snippet implemented for our Get Care feature. The location search function filters MKLocalSearchCompleter results that contain the keyword “United States”.

Figure 5 — Simplified Search location filter function

A location fetch is triggered when assigning search fragments to MKLocationSearchCompleter. The results are returned within the completerDidUpdateWithResults observable delegate and then are filtered by flat mapping (chaining) in the filterSearchLocation operation.

Figure 6 — Subscribe to MKLocationSearchCompleter query fragment search

The flatMap operator will then transform the items emitted by completerDidUpdateWithResults observable and flatten the emissions into a single array of MKLocalSearchCompletion observable. The chained observables results are retrieved on subscription within the onNext() and onError() callbacks.

The code snippet from Figure 6 is declarative. The behavior and logic is specified at the initialization of an observable, which prevents bugs in async event management. Each callback (next, error) on subscription is handled in sequence as events are single threaded. Along with pushing data streams to subscribers, observables can also return other observables. Chaining of observables is beneficial in writing readable code as the output of one asynchronous operation is fed into another operation. In our app, chaining observables are implemented in the network layer as we execute multiple sequential API requests. Strongly typed observables aid in self-documenting our code and reducing overall bugs.

Figure 7 — Location Search Demo

Conclusion

Reactive programming sounds great so far, but it has some cons. RxSwift has a learning curve. It is not a golden hammer for all asynchronous problems, however we found it to be very beneficial in replacing KVO and state machine patterns. We have not adopted Rx in managing our UI with data binding as UIKit core delegate patterns are easy to use. Because of this, on-boarding new hires remains easy as engineers who are unfamiliar with RxSwift do not have to learn a new programming pattern or style.

I would highly encourage teams to explore reactive programming. If you or your team have reservations using RxSwift in your project, introduce it selectively, like in the Network layer. By chaining RxSwift’s request operators, timeouts, exponential backoffs, and explicit error handling can be implemented easily.

--

--