Concurrency is an issue that comes up over and over in mobile development. iOS has threads, locks, run loops, callbacks, delegates, Grand Central Dispatch— a gallimaufry of ways to specify what should happen at some point in the future. Swift has closures, which can be called at some point in the future by any other piece of code that receives the closure. At some point Swift will further support concurrency with async/await and more.
Promises and Futures (PF) are constructs that allow the developer to “promise” that a result value will be provided at some point in the “future”, and then specify what to do with the promised results when that future arrives. They’ve proven to be useful in many contexts— so useful that PromiseKit is a popular solution for using PF in iOS and macOS apps. At the same time, Swift is open source and runs on Linux, and a growing number of developers are using and promoting it as solution for building servers using frameworks like Vapor.
Apple itself has even shown particular interest in PF, and has released an open source library called SwiftNIO, which stands for “Non-blocking I/O”, and which is aimed at keeping a server processing requests at maximal efficiency. At its heart is Apple’s very own implementation of Promises and Futures. The introduction to NIO states:
SwiftNIO is a cross-platform asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
Vapor itself moved to using NIO for its 3.0 release:
Vapor is now completely non-blocking and runs on Apple’s SwiftNIO. This means Vapor 3 is ready to handle high levels of concurrency when your application needs it.
“But I’m writing iOS apps, not servers” you say, “so I don’t need SwiftNIO, and besides, I have PromiseKit.” All good points, but consider:
- Apple says that NIO is for high-performance servers and clients. You’re writing an iOS app, which is a client.
- Apple also has introduced Network.framework, which will become the core of Apple’s future networking strategy.
- Apple also has introduced another framework built on top of NIO and Network.framework that provides first class support for iOS, macOS, and tvOS. It’s called NIO Transport Services (NIOTS).
NIO Transport Services are:
…an extension to SwiftNIO that provides first-class support for Apple platforms by using Network.framework to provide network connectivity, and Dispatch to provide concurrency.
So SwiftNIO is not only for the server, and it’s not only for networking— it’s for high-performance concurrency with a special emphasis on networking.
PromiseKit is great, but when Apple goes ahead and says, “Here’s how we’re doing promises and futures on our platforms,” I think the developer community ought to sit up and take notice.
Of course, NIOTS is not a standard part of any Apple platform yet— it’s an open source add-on. And it’s not quite as easy to use as import Foundation
… yet. But I think it’s going to keep evolving, and I have a hunch that it is really part of the way that iOS and macOS apps will be written in the future. That’s not a promise, but I’m pretty sure. 🙂
So how do we get started on iOS with NIOTS? This is a quick and hopefully somewhat gentle introduction. Future articles will cover useful ways of using NIO in your iOS apps.
WolfNIO
WolfNIO is a cocoapod that combines several components:
- SwiftNIO itself,
- SwiftNIOTransportServices,
- Vapor NIO Kit,
- A number of conveniences, some my own and some based on Vapor Async.
⚠️ You will also need Xcode 10.2 or later (including Swift 5).
WolfNIO has no other dependencies. You use it by adding this line to your app’s Podfile:
pod 'WolfNIO'
And in Swift files where you want to use it, simply:
import WolfNIO
Introductory Example
The WolfNIO GitHub repo contains an example iOS playground containing an introductory example we’ll walk through. Here are the steps to get it running.
First, in Terminal:
cd ~/Downloads/
git clone https://github.com/wolfmcnally/WolfNIO
cd WolfNIO/Example/
pod install
open WolfNIO.xcworkspace/
Then, in Xcode:
- Select one of the simulator targets like
iPhone Xs Max
and build the project:
- In the project navigator expand the workspace
WolfNIO
group and then click on the playground:
- Press ⌘-Shift-Y to show the debug output area below.
- Click on the Run button to execute the playground:
- A few seconds later you should see the following output:
0.5604 [background]: Fetched 5
1.0371 [background]: Fetched 2
2.0593 [background]: Fetched 3
2.06 [main]: Sum of fetched values: 10
2.0601 [main]: Done!
Examining the Output
A few things to notice about this output:
- The number at the start of each line is elapsed time. So this example took 2.06 seconds to execute. What was it doing all that time? Well, mostly waiting. And yet, none of this code performs delays, waits, or polling of results. Which means all that idle time is available for other useful work— or at least to save your battery.
- The
[background]
or[main]
indicator on each line shows whether that line was printed from a thread running in the background or on the main thread. As you should know, iOS and Mac apps have a special thread called main on which all user interface code is to be called. So it’s important to know which thread your call is running on, and it’s equally important to know when to move work off the main thread so as to not block the app from interacting with the user. - The example simulates fetching three pieces of data (integers) and then summing them up. These fetches could have happened via HTTP calls over the Internet, and they happen in the background. The reporting of results happens on main, and is where you’d updated your interface with the result of your asynchronous operations.
- In real life you usually can’t predict exactly when asynchronous operations will complete, or even in what order. Here we have three fetches, but we’ve set things up so the third fetch will complete first.
Now let’s go through the example piece by piece, starting at the top.
Walkthrough
Imports
import UIKit
import WolfNIO
import PlaygroundSupport
These are the necessary imports. UIKit
is there because this is an iOS playground, and in future articles we’ll talk about how to use PF to manage animations and other interface issues. PlaygroundSupport
is there so we can make sure the playground stops running when we tell it to— not when the last statement in it executes, as none of the lines of code in this playground pause and wait. The main through-line of the playground finishes immediately— everything else happens as the result of Promise
s fulfilling their Future
s— including the playground execution stopping.
Utilities
/// The time this playground started running
let startTime = Date().timeIntervalSinceReferenceDate
/// The elapsed time since this playground started running
var elapsedTime: TimeInterval { return Date().timeIntervalSinceReferenceDate - startTime }
/// A variation of `print()` that prefixes its arguments with the elapsed time since this playground started running
/// (rounded to the nearest 10,000th of a second) and an indicator of whether it’s running on the main thread
/// or a background thread.
func printLog(_ items: Any...) {
let message = items.map({ String(describing: $0) }).joined()
let time = (elapsedTime * 10_000).rounded() / 10_000
let name = Thread.isMainThread ? "[main]" : "[background]"
let prefix = [String(describing: time), name].joined(separator: " ")
print("\(prefix): \(message)")
}
/// Called at the start of a demo, tells the playground to run indefinitely (until `finish()` is called.)
func start() { PlaygroundPage.current.needsIndefiniteExecution = true }
/// Called at the end of a demo, tells the playground to finish execution.
func finish() { PlaygroundPage.current.finishExecution() }
These are utilities used by the rest of the playground.
EventLoopGroup
func makeBackgroundEventLoopGroup(loopCount: Int) -> EventLoopGroup {
return NIOTSEventLoopGroup(loopCount: loopCount, defaultQoS: .default)
}
In NIO, an EventLoopGroup
vends EventLoop
s, which in turn vend Promise
s, which in turn vend Future
s. This utility is used to create an EventLoopGroup
with a specified number of associated EventLoops
. Each EventLoop
wraps a Grand Central Dispatch DispatchQueue
, which in turn manages some number of threads.
Setup
/// This demo asynchronously mock-fetches three pieces of data (integers) and when all are present,
/// sums them and prints the result.
struct Demo1 {
// We’re going to perform the mock-fetches on a background `DispatchQueue` wrapped with an
// `EventLoopGroup`. `EventLoopsGroup`s vend `EventLoops` from their `next()` call, which can
// then be used to make `Promise`s.
let backgroundEventLoopGroup = makeBackgroundEventLoopGroup(loopCount: 3)
We’ll make each of our demos self-contained within their own type, in this case a struct
called Demo1
. (Currently there’s only this demo.)
The first thing we do is set up an EventLoopGroup
on which to do our background processing. This EventLoopGroup
contains 3 EventLoop
s, which are accessed in round-robin fashion by calling the EventLoopGroup
’s next()
function.
The Fetch
/// A function that performs a mock-fetch of an integer after a simulated
/// period of latency.
///
/// The fetch itself executes on a background thread, but the promise is
/// fulfilled on the main thread. Notice that none of these calls (even the ones
/// on the background thread) actually block.
func mockFetch(returning value: Int, afterSeconds seconds: TimeInterval) -> Future<Int> {
// Make the promise that will deliver the results of the fetch on the main event loop.
let promise = MainEventLoop.shared.makePromise(of: Int.self)
// Schedule the fetch on a background event loop.
backgroundEventLoopGroup.next().scheduleTask(in: .milliseconds(TimeAmount.Value(seconds * 1000))) {
// This executes later on a background thread, and fulfills the promise with the result.
printLog("Fetched \(value)")
promise.succeed(value)
}
// This executes immediately and returns the Future associated with the promise.
return promise.futureResult
}
This method takes the value that will be mock-fetched, and an interval that should pass before its future should be fulfilled. The Future<Int>
it returns indicates that the result of the fulfilled future is indeed an Int
.
This demo simulates a network fetch, like an HTTP GET. Often when a GET returns we want to update something in the UI, so we want that work to happen on the main thread. WolfNIO
provides a class that helps promises always complete on the main thread, called MainEventLoop
. Like any singleton, it has an attribute which is used to access its sole instance: shared
. Having that, we call makePromise()
, promising to return an Int
as our result.
Promises can also fail, in which case code can be written to handle the failure cases, similar to how thrown exceptions are handled with do/catch
constructs. None of the code we’re examining here fails— that’s a topic for future articles.
It’s very important that code running in event loops never block. UIView
animations, URLSession
tasks, and many other system services provide non-blocking ways of performing tasks in the background and reporting back later with results. PF is a way of unifying this. For now, we’re simply dispatching a task to complete on a background thread after a certain amount of time elapses. Once it executes, it will print a message indicating a successful fetch, and then fulfill the promise with the value that was specified. Note that the fetch task will execute on a background thread, but the Promise
will be fulfilled on the main thread, because it was created from the MainEventLoop
.
The last line of the method returns the Future
associated with the Promise
. Everything that happens when this method is called happens immediately— that is, the method takes almost no time to execute.
The Main Line of the Demo
/// Runs the demo. Completes on the main thread.
func run() -> Future<Void> {
// Kick off the fetching of our three integers. Note that these calls do
// not block and return immediately.
let future1 = mockFetch(returning: 2, afterSeconds: 1)
let future2 = mockFetch(returning: 3, afterSeconds: 2)
// Note this future kicks off third, but fulfills before the others.
let future3 = mockFetch(returning: 5, afterSeconds: 0.5)
// Register what to do when all the futures have succeeded.
// `whenAllSucceed()` transforms an array of futures into a
// future array of results.
return Future.whenAllSucceed([future1, future2, future3], on: MainEventLoop.shared).map {
let sum = $0.reduce(0, +)
printLog("Sum of fetched values: \(sum)")
}
}
This method actually runs the demo. Like the mockFetch()
method, it returns immediately, and returns a Future<Void>
. This strange animal is simply a Future
with no particular result— it simply succeeds or fails.
The three calls to mockFetch()
kick off futures which when fulfilled return the integers 2, 3, and 5 respectively. It returns them 1 second, 2 seconds, and half a second after they start. This means that future3
will actually complete first.
We want to ultimately sum the result of all the fetches. But at this point we’ve got three Future<Int>
s. How do we execute something when all three of them are fulfilled? For this we first call the static method Future.whenAllSuceeed()
and pass it an array of our three futures. It then transform this [Future<Int>]
(that is, an array of future Int
s) into a single Future<[Int]>
(that is, a future array of Int
s.)
Now we have a single future that, when fulfilled will provide us with all the results we’re looking for. But we still have to wait for that future to be fulfilled. So we chain the result of whenAllSucceed()
to a call to map()
, which maps the Future<[Int]>
to our ultimate [Int]
, which is the implicit argument $0
to the closure.
Finally, the closure sums the array of Int
s using reduce()
and prints the result. Don’t get confused here— the end of this closure is not the end of the run()
method— that actually happened in the distant past, two seconds ago just after the three mockFetch()
calls kicked off their Future
s.
The Top Level
start()
Demo1().run().always {
printLog("Done!")
finish()
}
When the playground executes, it first calls start()
, which tells the playground not to stop executing simply because all the statements in it have completed.
Next, we instantiate a Demo1
and call its run()
method, which as we saw above kicks off a series of futures and returns a single Future<Void>
that when fulfilled signals the completion of the demo.
We then chain that Future<Void>
to a call to always()
, which as its name suggests always runs after the Future
completes, regardless of whether the completion was successful or not. In this demo, it will always succeed, but in any case we still want to print Done!
and then call finish()
, which instructs the playground to cease execution. Again, the print and the call to finish()
will happen seconds after this top-level statement executes.
I hope this has piqued your curiosity about PF, and where Apple is headed with it. I look forward to your comments and questions!
🐺