Introducing Flow — Asynchronous programming made easy

Måns Bernhardt
6 min readApr 16, 2018

--

Today we are proud to announce the open sourcing of Flow, a Swift framework for building and maintaining complex asynchronous flows. Flow is modern, lightweight and composable, allowing you to write code that is more robust and easier to reason about. It has been field-tested and has matured over the course of many years while being extensively used in iZettle’s point of sale app to solve real problems.

If you ever wondered what this thing about reactive programming is, now is the time to give it a try. We hope that it will give you the boost in productivity and joy of developing software that it did for us.

So what is Flow about?

Modern applications often contain complex asynchronous flows and life cycles. Flow aims to simplify building these by solving three main problems:

Lifetime management — Managing long-living resources

In Flow, lifetime management refers to the handling of for how long a resource is kept alive. This becomes especially important when working with closures, as they, in turn, capture and keep resources alive. Instead of weakly capturing those resources, Flow advocates the use of explicit disposal of resources. At first, this might seem like adding a lot of extra work, but once you adopt it in your application, your code will be much easier to reason about.

In Flow, the Disposable protocol is the standard way of handling clean-up after something is done. The protocol only declares one method dispose() to be called to perform clean-up:

protocol Disposable {
func dispose()
}

By returning a Disposable when starting long living processes, you have a standard way of handling clean-up:

extension UIView {
func showSpinnerOverlay() -> Disposable {
let spinner = ...
addSubview(spinner)
return Disposer {
spinner.removeFromSuperview()
}
}
}

let disposable = view.showSpinnerOverlay()

disposable.dispose() // Hide spinner

Event handling — Observing events over time

APIs that want to notify the user of changes are common. Apple provides several of those and they come in many forms, such as notifications, delegates, target/action and KVO. In Flow, event-based APIs are represented by signals. Since a signal hides the underlying logic and provides a uniform API, not only can you use them interchangeably, but you can also easily pass them around, combine them, and transform them into new ones.

By calling onValue() on a signal you will start listening on new changes until you dispose the returned disposable:

let bag = DisposeBag() // Bag of disposables

// UIButton provides a Signal<()>
let loginButton = UIButton(...)

bag += loginButton.onValue {
// Log in user if tapped
}

// UITextField provides a ReadSignal<String>
let emailField = UITextField(...)
let passwordField = UITextField(...)

// Combine and transform signals
let enableLogin = combineLatest(emailField, passwordField)
.map { email, password in
email.isValidEmail && password.isValidPassword
} // -> ReadSignal<Bool>

// Use bindings and key-paths to update your UI on changes
bag += enableLogin.bindTo(loginButton, \.isEnabled)

Asynchronous operations — Handle results that might not yet be available

When building applications interacting with a user, a goal is to keep the application responsive at all times. It is thus important to not perform long-running operations that will block the UI. This is one reason why many APIs used in UI intensive applications are asynchronous.

Working with asynchronous code can easily become messy as soon as you need to nest a few operations which depend on each other’s results. So similar to signals, Flow abstracts the idea of an asynchronous operation and encapsulates it into a Future<T> type representing a result that might not yet be available:

extension URLSession {
func data(for request: URLRequest) -> Future<Data> {
return Future { completion in
let task = self.dataTask(with: request) { data, _, error in
if let error = error {
completion(.failure(error))
} else {
completion(.success(data!))
}
}
task.resume()
return Disposer { task.cancel() }
}
}
}

Futures are highly composable and comes with many useful transformations:

func login(email: String, password: String) -> Future<User> {
let request = URLRequest(...)
return URLSession.shared.data(for: request).map { data in
User(data: data)
}
}

login(...).onValue { user in
// Handle successful login
}.onError { error in
// Handle failed login
}

Bringing it all together

By bringing these three concepts together we can build complex UI flows that are still easy to reason about:

class LoginController: UIViewController {
let emailField: UITextField
let passwordField: UITextField
let loginButton: UIButton
let cancelButton: UIBarButtonItem

var enableLogin: ReadSignal<Bool> { // Introduced above }
func login() -> Future<User> { // Introduced above }
func showSpinnerOverlay() -> Disposable { // Introduced above }

// Returns future that completes with true if user chose to retry
func showRetryAlert(for error: Error) -> Future<Bool> {
return Future { completion in
let retry = UIAlertAction(title: "Retry") { completion(.success(true)) }
let cancel = UIAlertAction(title: "Cancel") { completion(.failure(error)) }
let alert = UIAlertController(...)
// Present alert ...
return Disposer { // Dismiss alert }
}
}

// Will setup UI observers and return a future completing after a successful login
func runLogin() -> Future<User> {
return Future { completion in // Completion to call with the result
let bag = DisposeBag() // Resources to keep alive while executing

// Make sure to signal at once to set up initial enabled state
bag += self.enableLogin.atOnce().bindTo(self.loginButton, \.isEnabled)

// If button is tapped, initiate potentially long running login request
bag += self.loginButton.onValue {
self.login()
.performWhile {
// Show spinner during login request
self.showSpinnerOverlay()
}.onErrorRepeat { error in
// If login fails with an error show an alert...
// ...and retry the login request if the user chose to
self.showRetryAlert(for: error)
}.onValue { user in
// If login is successful, complete runLogin() with the user
completion(.success(user))
}
}

// If cancel is tapped, complete runLogin() with an error
bag += self.cancelButton.onValue {
completion(.failure(LoginError.dismissed))
}

return bag // Return a disposable to dispose once the future completes
}
}
}

Looking back — on being pragmatic

Flow has a very pragmatic design philosophy. It has been evolved over the years to solve the problems we have encountered while growing the iZettle app. It started many years ago, way before Swift, where we tried to make the complexity of our card payment flow easier to maintain and understand. Performing a card payment is a complex coordination of different asynchronous and concurrent operations. It involves hardware of different kinds, networking, timeouts, and user interaction. There are many things that can go wrong and we need to be able to gently tear down any ongoing processes while also propagating errors correctly. This resulted in the design of the Future type that later was modernized and brought over to Swift and Flow.

Power of abstractions

Once you have the right abstraction and an ergonomic API for working with asynchronous operations, you start to find uses for it everywhere. One great example is the presentation of view controllers. But perhaps more common when working with UI is to observe events over time. Events manifest in many different forms, such as button presses, notifications, and KVO updates. Here again, we used our experience from our Objective-C, block-based APIs, to develop a more general Swift solution using the Signal type.

When working with UI there is also a need to clean things up after e.g. a presentation. This is especially true when working with a lot of closure based APIs. Where previously we were trying to solve this by using weak references, this was a never-ending chase of fixing memory leaks. The solution turned out to be a very simple idea, but yet so powerful, solved by introducing the Disposable protocol.

Increased productivity

From introducing better support for lifetime management, signals and futures, our code base got a renaissance. An explosion of new possibilities and ideas grew out of these basic concepts, resulting in many powerful frameworks that have drastically changed how we build software at iZettle. Today we feel much more confident in our code, as it is easier to read, understand and reason about. It is easier than ever to decompose hard problems into smaller and more reusable components. Overall the joy of writing code and the safety of trusting your frameworks has been a huge boost in productivity.

Learn more

Flow can be found at GitHub where you can try it out yourself and learn more about how to use it. We will also post a series of articles focusing on API design, where we will, step by step, derive the core types of Flow.

Our hope is that this article has inspired you to think about asynchronous programming in new ways. If you liked what you have seen so far, we encourage you to explore Flow further. If you find it useful, we welcome feedback and why not suggest or even contribute with your own improvements.

--

--