RxSwift Action

FRP and RxSwift

FRP has a lot of benefits over typical iOS development patterns: declarative nature, redused state complexity, ease of doing async work, unified data flow handling, etc. One of the implementations of FRP for iOS is RxSwift. RxSwift is a great "cannonical"1 implementation of Reactive Extensions in Swift. You can learn more about RxSwift and why it's important here. In this article we'll take a look at how to implement a complex files download operation with RxSwift and a pod that helps with it called Action.

Complex operations with RxSwift

FRP makes a lot of things like data subscribtion and observation easier. There is a lot of RxSwift examples that can be found here https://github.com/ReactiveX/RxSwift or on other wiki pages but they all showcase more or less simple use cases. Sometimes when you need to do something not as straightforward and trivial with FRP it can become unwuildy and difficlut to wrap your head around. Naive implementation is not always good and errorless as it may seem.

Problem

Consider this example: there's an iOS app. It shows users their itineraries. It gets HTML and maps data (some images and/or binary data) for those itineraries from a backend API when it's online. When the app is offline it can work with HTML and maps data saved to disk. The user can tap a button in a cell in a table view to download that HTML and maps data. The user can see download progress while files are downloading for offline use.

Naive solution (i.e. simple Observables)

At first you might think - oh, it's simple, I'll just create one Observable for downloading HTML files and another Observable for downloading map files.
Yes, this is a good first step. At least you're on the right path, you are trying to break the whole operation down into smaller components that can be handled individually and can work in isolation if necessary. At the end of the day you probably have got Observables like these:

func HTMLFilesDownloadSignal(itinerary: JRItinerary) -> Observable<Int> {  
  return Observable.create({ [unowned self] (subscriber) -> Disposable in
    // ... HTML download code here ... probably some Alamofire stuff...

    // each time your code donwloads another piece HTML file it will call
    subscriber.onNext(progress)
    // where progress is an Int from 1 to 100 
    // it idicates how many HTML files percentage wise is left to download

    // eventually when the HTML files donwload is done this Observable will call
    subscriber.onCompleted()
  })
}

func mapDownloadSignal(itinerary: JRItinerary) -> Observable<Int> {  
  return Observable.create({ [unowned self] (subscriber) -> Disposable in
    // ... map download code here ...

    // each time your code donwloads another piece of the map it will call
    subscriber.onNext(progress)
    // where progress is an Int from 1 to 100 
    // it idicates how much of the data percentage wise is left to download

    // eventually when the map data donwload is done this Observable will call
    subscriber.onCompleted()
  })
}

As you can see here we omit the actual details of HTML and map download code but most likely for HTML you'll use Alamofire or similar pod to download HTML files. For map data you could consider Mapbox as a solution. To reitirate, we have two methods here that create Observables for respective parts of our overall operation for downloading offline data. HTMLFilesDownloadSignal creates an Observalbe that emits Int events every time it loads the next 1% of overall HTML files that need to be donwloaded. mapDownloadSignal creates an Observable that emits Int events every time it loads the next 1% of overall map data that needs to be donwloaded.

Ok, so now we've got our Observables for each part of the operation. The next thing to do is to "glue" them together into one. One way to do it is to use Observable.combineLatest. Since both of our Observables emit Ints that go upto 100% we we can use combineLatest and divide them in half. CombineLatest is perfect for that task because it will start emitting values as soon as one of our signals2 does, it's quite handy because both of them are async and can be executed concurently.

func downloadOfflineItinerarySignal(itinerary: JRItinerary) -> Observable<Int> {

  return Observable.combineLatest(
      HTMLFilesDownloadSignal(itinerary).startWith(0), 
      mapDownloadSignal(itinerary).startWith(0)) 
    { (progress1, progress2) -> Int in            
      return (progress1 + progress2) / 2            
    }

}

Notice how we use startWith(0) for both of them. We need it because combineLatest will start emmiting event when at least one of the observalbes it combines emits a value. That means that our HTML signal could start before even a single event from mapDownloadSignal is received. To prevent that we chain it with startWith(0) to give them a "default/initial" value.

NOTE: this is an example from a real life, in production, application.  
I will not get into the details of what JRItineraries are. I'll only say that  
they are models and represent some data the user can work with in the app.  

Now we have our combined signal that represents a download operation and we can start("execute") it by subscribing to it like this:

downloadOfflineItinerarySignal(itinerary).subscribe(onNext: { (progress) -> Void in                

  // do UI progress updates here

}, onError: { (error) -> Void in

  // show an error message 

}, onCompleted: { () -> Void in 

  // show "completed" UI state

}).addDisposableTo(disposeBag)
NOTE: I will not get into details of how and why use `disposeBag`.  
In this article we'll just use a `disposeBag` instance assuming that  
it was created somewhere in at the right time and place.  

It's all nice and good and we have the desired result but there's a fundamental flaw with this approach - if you have several observers for the same downloadOfflineItinerarySignal they all will start execution of that signal when they subscribe. That will result in multiple calls to download the same data if we use it in let's say a table view cell or similar situation.

What we really want is some sort of an abstraction or a thing that represents a download operation that can be started/launched once and then subscribed to observe progress of the operation. And every subsequent subscription shouldn't trigger or relaunch that operation.

Second attempt. Use Connectable Observable.

Turns out there's a way to achieve exactly what we want - have a work/operation be represented by a download signal we just created above and have it be subscribable without relaunch side-effects. That things is called Connectable Observable3.

Connectable Observable is a special kind of Observable. It allows you to create an Observable that will start executing and emitting values when connect() method is called regardless of its subscribers. And every time a new subscriber subscribes to it it will start sending that subscriber the current items instead of relaunching the whole thing. In other words - perfect for our use case!

To create a Connectable Observable with for our offline download operation we'll do the following:

func downloadOfflineItinerarySignal(itinerary: JRItinerary) -> ConnectableObservable<Int> {

  return Observable.combineLatest(
      HTMLFilesDownloadSignal(itinerary).startWith(0), 
      mapDownloadSignal(itinerary).startWith(0)) 
    { (progress1, progress2) -> Int in            
      return (progress1 + progress2) / 2            
    }.publish()

}

The only thing we did really is to add .publish() before returning our download signal. That will convert it into a ConnectableObservable.

And we can launch it like this:

let signal = downloadOfflineItinerarySignal(itinerary)

signal.connect()

That will start execution of our download operation and if anyone wants they can subscribe before or after connect() method was called.

let signal = downloadOfflineItinerarySignal(itinerary)

signal.subscribeNext { (progress) in  
  // do some stuff here when progress update will come
  // this "next" callback will receive all the values of progress
  // because we've subscribed before connect() method call
}.addDisposableTo(disposeBag)

signal.connect()

////////
/// .....
/// somewhere else in code
/// .....
//////// 

signal.subscribeNext { (progress) in  
  // this "next" callback will receive only the progress values after subscription  
  // because we've subscribed after connect() method call
}.addDisposableTo(disposeBag)

In the example above we subscribed to our download signal twice. The first subscription will receive all the values because it was created before connect() method call. And the second subscription will receive progress values only from the moment in time when it was created because it was created somewhere else in code after the connect() method call.

Ok, so now we have a better solution than our first attempt. Our download signal can be "connected to" and every time a new subscription is created it won't spin a download over and over again.

But we're not done yet. Remember, our requirement is to use that signal in a UITableViewCell. Users should be able to tap a button in every cell to download respective itinerary. Now with the new ConnectableObservable we can do that by first subscribing to progress events from our download signal and then calling connect() method on button tap as follows:

// somewhere in your view controller

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {  
  let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! MyTableViewCell

  let signal = downloadOfflineItinerarySignal(itinerary)  

  signal.subscribeNext { (progress) in
    cell.updateWithProgress(progress)
  }.addDisposableTo(disposeBag)

  cell.donwloadButton.rx_tap.subscribe(onNext: { [unowned self] () -> Void in
    signal.connect()
  }).addDisposableTo(disposeBag)

}

Whoohoo, now it should all finally work!!! Amm, not quite exactly. It does what we want but it has several flaws and one of them is the fact that there's nothing stopping the user from tapping donwloadButton multiple times and starting download signal for the same data over and over again.
We don't want that and an obvious solution could be to create some kind of a state to keep track of download progress and then based on that state enable and disable donwloadButton so that the user can tap it too many times. But this is what we are trying to avoid in the first place with FRP, state!

Better solution with RxSwift Action

Turn out that there's an even better solution that covers a lot of cases and issues we've encountered with our unorthodox approach. It's a pod called Action.

This pod implements a Command Pattern using RxSwift Observables that wraps our custom signal and provides a lot of nice things like enabled signal to enable/disable buttons, and many others. Perfect for our case.

You can install it just like any other pod by adding it to your Podfile and running pod install.

Action give works nicely with our existing signal. The only thing we need to do to convert our download signal into action is to wrap it inside one like this:

func downloadOfflineItineraryAction(itinerary: JRItinerary) -> Action<Void, Int> {  
  return Action(workFactory: { [unowned self] (input) -> Observable<Int> in
    return downloadOfflineItinerarySignal(itinerary)
  })
}

Oh, and also we should make our downloadOfflineItinerarySignal back to a regular observable again by removing publish() method call.

func downloadOfflineItinerarySignal(itinerary: JRItinerary) -> ConnectableObservable<Int> {

  return Observable.combineLatest(
      HTMLFilesDownloadSignal(itinerary).startWith(0), 
      mapDownloadSignal(itinerary).startWith(0)) 
    { (progress1, progress2) -> Int in            
      return (progress1 + progress2) / 2            
    }

}

As usual we create an action by just calling our method.

let downloadOfflineItineraryAction = downloadOfflineItineraryAction(itinerary)

And we can run it by calling execute() instead of connect().

let downloadOfflineItineraryAction = downloadOfflineItineraryAction(itinerary)

downloadOfflineItineraryAction.execute()

Action provides us several useful signals that we can subscribe to to observe actions behavior and state change.
Useful for us would be enabled, elements, and errors signals.

  • enabled will emit true/false values when action is enabled or disabled, for example if it's executing it will send enabled false signal. Perfect for us to disable our button so that the user can't tap and start download multiple times.

  • elements will emit new values from the underlying Observable that does the actual work. In our case downloadOfflineItinerarySignal with its progress values.

  • errors will emit errors from the underlying Observable, in our case if downloadOfflineItinerarySignal fails.

In practice it can be used with our cell like this:

// somewhere in your view controller

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {  
  let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! MyTableViewCell

  let downloadOfflineItineraryAction = downloadOfflineItineraryAction(itinerary)

  downloadOfflineItineraryAction.enabled.bindTo(cell.downloadButton.rx_enabled).addDisposableTo(disposeBag)

  downloadOfflineItineraryAction.elements.subscribeNext({ [weak self] (progress) in            
      cell.updateWithProgress(progress)
  }).addDisposableTo(disposeBag)

  downloadOfflineItineraryAction.errors.subscribeNext({ [weak self] (error) in    
    cell.updateWithError(error)
  }).addDisposableTo(disposeBag)

  cell.donwloadButton.rx_tap.subscribe(onNext: { [unowned self] () -> Void in
    downloadOfflineItineraryAction.execute()
  }).addDisposableTo(disposeBag)

}

Let's go through what we've done above:

  1. we create an action
  2. we bind cell's downloadButton enabled property, using convenient methods rx_enabled and bindTo from RxCocoa, to the values of downloadOfflineItineraryAction.enabled signal. This effectively sets enabled state of our button to true or false depending on whether our action is currently executing or not. This will prevent users from restarting it over and over again while it executes.
  3. we observe downloadOfflineItineraryAction.elements signal and update cell with download progress data just like we did before with out own custom Observable.
  4. for errors we observe downloadOfflineItineraryAction.errors signal. It will emit values if our download signal fails and returns an error. Gives us a chance to update cell's UI with an error message of some kind.
  5. we run downloadOfflineItineraryAction.execute() on download button tap to start the download operation.

Note about CocoaAction

Action pod has a convenient implementation of Action called CocoaAction that can be attached to a UIButton (using convenient method rx_action) and it will be executed automatically when the button is pressed. The advantage of using it is that most of the setup we've done above will be handled internally by the button. The disadvantage though is that we cannot observe download progress report of our Observable.
Turns out the implementation of that rx_action method that binds an Action to a button is fairly straight froward and all we need to do is these 3 things:

  1. nullify dispose bag to get rid of previous subscriptions
  2. bind enabled state of the Action to rx_enabled of the button so that it can be clicked only when appropriate action.enabled.bindTo(self.rx_enabled).addDisposableTo(self.actionDisposeBag)
  3. add rx_tap observer to call execute() on the button when it's tapped.

All of these but dispose bad flushing we've done in our implementation.

Conclusion

That is it! We know have got a robust download operation that manages its own state and lets us know when it's executing, enabled/disabled, got new values, or errored out. Action is a very nice wrapper around plain Observables and can be used in many situations where heavy computation needs to be performed whether it's network requests or local file storage operations or something else.

  1. reactivex group has done a lot of great work making a lot of Rx implementaionins in different langauges. You can find a list them here http://reactivex.io/languages.html

  2. I use terms Observable and Signal interchangably. Observable is an Rx term. Signal is a ReactiveCocoa term. They mean the same thing.

  3. See Connectable Observable Operators section for more details http://reactivex.io/documentation/operators.html#connectable

comments powered by Disqus