Optimistic Networking with Reactor

A Love Story

Last time we talked about some pretty cool stuff we can do to build a more reactive iOS application using the Reactor architecture. Today we’ll investigate how we can keep in sync with data from the network and make our app faster with the power of optimism!

Let’s look at the Reactor architecture diagram for a quick refresher:

┌──────────────────┐                                 ┌──────────────────┐
│                  │                                 │                  │
│                  │         ┌───────────┐           │                  │
│                  │◀────────┤   Event   ├───────────┤                  │
│                  │         └───────────┘           │                  │
│                  │                                 │                  │
│                  │                                 │                  │
│     Reactor      │                                 │    Subscriber    │
│                  │                                 │                  │
│                  │         ┌───────────┐           │                  │
│    ┌───────┐     ├─────────┤  update   ├──────────▶│                  │
│    │ State │     │         └───────────┘           │                  │
│    └───────┘     │                                 │                  │
│                  │                                 │                  │
└──────────────────┘                                 └──────────────────┘

view rawreactor_diagram.md hosted with ❤ by GitHub

We send events to make the Reactor update our application state. The reactor handles events, updates the state, and then notifies all the subscribers. But what about asynchronous events?

Commands

Performing async events is likely going to be a common occurrence in your app. Reactor offers the Command API to help establish a consistent and safe way to perform asynchronous events

struct FetchPlayers: Command {
var session = URLSession.shared
func execute(state: State, reactor: Reactor) {
session.dataTask(with: fetchPlayerURL) { data, response, error in
if error {
reactor.fire(event: DisplayError(message: "Error fetching players"))
}
else {
var players: [Player] = // handle json...
reactor.fire(event: Update(value: players))
}
}.resume()
}
}
// Caller uses:
reactor.fire(command: FetchPlayers())

view rawCommand.swift hosted with ❤ by GitHub

Hopefully the code inside execute looks similar to your usual networking functions. The difference here is we’ve wrapped it inside of a function which receives its own copy of the application state and reference to the Reactor. We don’t end up needing the state for anything here, but we use the Reactor reference to dispatch events upon network task completion.

Instant, Optimistic Network Results

Say we’re performing a different type of network request, perhaps adding a new player. Do we really want to wait for a response from the server before we can update our UI? Can you imagine liking a post on Facebook and waiting for the request to travel all the way to the Facebook and back to confirm our like was successfully recorded before seeing the thumb turn blue? Ain’t nobody got time for that! We want our app to feel fast and responsive!

What if we just assume the network request is successful and immediately update the UI? Then we can update with the real response later. Or, in the case of an error, we can reset to the original state. Let’s call this “optimistic networking.”

struct AddPlayer: Command {
var session = URLSession.shared
var player: Player
func execute(state: State, reactor: Reactor) {
let originalPlayers = state.players
let task = session.dataTask(with: player.putRequest()) { data, response, error in
if error {
// 3. whoops. switch back and display error
reactor.fire(event: Update(value: originalPlayers))
reactor.fire(event: DisplayError(message: "Error adding \(player.name)"))
}
else {
// handle json and ...
// 2. update with the real values from the server, hopefully the same
reactor.fire(event: Update(value: players))
}
}
// 1. optimistically assume success and immediately update UI
var optimisticPlayers = originalPlayers
optimisticPlayers.append(player)
reactor.fire(event: Update(value: optimisticPlayers))
task.resume()
}
}
// Caller uses:
reactor.fire(command: AddPlayer(player: player))

view rawOptimisticCommand.swift hosted with ❤ by GitHub

This code is similar to our FetchPlayers command, but we have added properties to our struct—this allows us to set up necessary data for our async Command. We also are firing events in a few more places:

  1. We optimistically add the new player to our state, and fire an event telling the reactor to update its current list of players with our optimistic list.
  2. Upon a successful request, we parse the response and update (for a second time) with the result the server returns. If the data is the same (if our optimism was correct) then we should see no difference.
  3. If our request came back with an error, then we reset the list back to where it was when we started, and display an error message to the user.

Conclusion

If you are using Reactor and doing any sort of of asynchronous operations then you want to use Commands!

Commands will help you handle async tasks in a consistent manner while leveraging the power of Reactor to keep your UI always in sync. And with almost no work at all, you can have the power of optimism to make your app feel faster.

Read more