Observables, State Machines, and Associated Types… Oh My!

Jamie Pinkham
Snagajob Engineering
4 min readJan 24, 2017

--

Snagajob’s iOS team uses RxSwift to build their apps (our Android team uses RxJava and is hiring!), and one of the most common questions that comes up during building apps using RxSwift is how to deal with errors. In the observable world, errors end the stream that an observable produces. Often, these errors don’t actually represent a permanent error condition, but a transient or recoverable error state. Some examples of this are the network being unavailable or unexpected status codes from our web server.

In our code base we have two types to manage these conditions. The first is a Result type that is generic over the value and error. It allows us to treat error conditions as a potential event on our observable stream, and prevents us from completing the observable stream in the case of a recoverable error. This result type is heavily inspired by Rob Rix's fantastic Result microframework. This is a great type for handling API responses.

On the other hand, a large part of our programming time is spent describing these states to users via UI. Which is why we’ve also built our ViewState type. The ViewState type is an enum that specifies the different states of a view. The possible states are:

  • .result – we were able to complete the request. This is the “happy path”.
  • .loading – we’re fetching results from the network or some other data provider
  • .error – some recoverable error occurred
  • .empty – no results were found

Thanks to Swift’s associated values, the .result case includes the actual result value–the data we want to display in the view. This data could be a list of jobs, a user's detail information, or anything else. To allow our enum to carry the data we need, we make the ViewState enum generic over the result type.

Coupled with this is a protocol we call ViewStateTransitionable. The protocol requires the implementor to define the result type it intends to display and a UIBindingObserver that manages the state transitions for each state emitted by the source observable.

RxSwift provides us with UIBindingObserver that gives us a few guarantees to the Observer’s properties. These are important to building UIs using Observables:

  • can’t bind errors (in debug builds binding of errors causes fatalError in release builds errors are being logged)
  • ensures binding is performed on main thread (it will dispatch to the main queue if the observable’s values are delivered on a non main queue thread)
  • doesn’t retain the source target

To demonstrate the use of this system, we’ve created an example in this repo. It’s a simple view controller subclass that sets up an Observable by scanning over the tap observable of a UIBarButtonItem and adding 1 to it.

self.refreshItem.rx.tap
.scan(0) { previousValue, _ in
return previousValue + 1
}

We then map that int value to produce one of the values of our ViewState enumeration, producing an error every 10th tap, a random color from an array or an empty state for other taps:

.map { int -> ViewState<UIColor> in
if int % 10 == 0 {
return .error(DemoError.something)
} else if i % 2 == 0{
let randomColor = arc4random_uniform(UInt32(self.backgroundColors.count))
return .result(self.backgroundColors[Int(rand)]
} else {
return .empty
}
}

We then promote that raw Observable into a Driver. You can read more about the driver unit here. Simply stated, a Driver is an observable type with the following important guarantees:

  • Can’t error out
  • Observe on main scheduler

In other words, Drivers are helpful for providing an execution context that’s appropriate for Observables that are used to “drive” UI elements.

In order to promote a plain Observable, you need to provide some logic as to what should happen when the source Observable produces an error. In our example, we wrap that error in our ViewState enum value called .error, as seen here:

.asDriver(onErrorRecover: { observableError in
return Driver.just(.error(observableError))
})

At this point we have an Driver<ViewState<UIColor>> that needs to be utilized. We can bind the Driver to our ViewControllers implementation of ViewStateTransitionable’s viewState: UIBindingObserver which simply hides and shows various views the view controller is managing. You can see that in the implementation of viewState in the extension that adds ViewStateTransistionable conformance to our view controller. This is also where we define the protocol’s required associatedtype which defines the expect data type we will be displaying.

extension ViewController: ViewStateTransitionable {typealias Result = UIColor
typealias UIElement = ViewController
var viewState: UIBindingObserver<UIElement, ViewState<Result>> {
return UIBindingObserver(UIElement: self) { viewController, state in
switch state {
case .result(let color):
self.resultView.backgroundColor = color
UIView.transition(with: self.resultView, duration: 0.2, options: [], animations: {
self.view.bringSubview(toFront: self.resultView)
}, completion: nil)

case .empty:
UIView.transition(with: self.emptyView, duration: 0.2, options: [], animations: {
self.view.bringSubview(toFront: self.emptyView)
}, completion: nil)
case .error:
UIView.transition(with: self.errorView, duration: 0.2, options: [], animations: {
self.errorLabel.text = "An error occurred"
self.view.bringSubview(toFront: self.errorView)
}, completion: nil)
default: return
}
}
}
}

We think this is a really cool example of how RxSwift, combined with Swift’s static type system, (including associatedtypes, generics and protocols) allows us to compose unique, type-safe and concise code that allows us to describe our problems in a semantic manner.

--

--

Jamie Pinkham
Snagajob Engineering

I like wings and baseball. I drink natural light, unironically Opinions are my own.. blah blah blah