Would Async/Await Work In Swift/iOS?

swift

In this post, we will discuss if Async/Await work in Swift/iOS. Today, Swift doesn’t have a language-level solution to writing asynchronous code. If it did, this is the article I’d write to explain how it works. (Probably in Swift 7 – after we get more codemojis and renamed methods first.)

In an iOS application, it is common to grab some data from a web service. We typically do it like this:

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)

  Store.instance.fetchAccounts { result in
      switch result {
      case .success(let accounts):
        self.accounts = accounts    
      }

      self.tableView.reloadData()
  }
}

This code will make a web service request each time this view controller will appear on the screen. When that request is complete, some code to use the result is executed.

This code describes how two specific events are reacted to. Event handling code often triggers new events and sets up their reactions with more code. In the above code sample, the viewWillAppear method will get executed for a very specific event: when an instance of this view controller is about to go on the screen. This method sends an HTTP request and registers code to be executed for an even more specific event: when that particular request completes.

Related: Aqueduct – An Open-Source, Server-Side Web Framework written in Dart

In between viewWillAppear finishing and the HTTP request completing, new events – like the user tapping the screen – can be received and reacted to elsewhere. We really can’t be sure how long it will take before the HTTP request is complete or how many other events may be received during that period. But one thing we can be sure of is the order in which each of those lines in viewWillAppear gets called.

Managing the order of operations in asynchronous code like this is an important task in applications today because information is often stored somewhere other than the device itself – and it takes time to get it. Declaring callback closures is the typical way of setting up how an event should be reacted to, but it isn’t the only way. After all, it wasn’t that many years ago that Objective-C didn’t yet have blocks and asynchronous behavior was accomplished through a series of delegate methods.

Some languages, like C# and Dart, have an even better way to handle asynchronous code: the async and await syntax. If Swift had async/await, you would be able to implement the above code like so:

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)

  accounts = await? Store.instance.fetchAccounts()
  tableView.reloadData()
}

Notice that instead of providing a callback to fetchAccounts this code awaits the return value and then goes on to do the work that was in the callback. The return value of awaiting on fetchAccounts is the array of accounts. This leads to more synchronous-looking code and is really valuable when callbacks start to get nested.

In order for a language to implement the await keyword, it requires a special type called a Future<T>, which you may be familiar with.  A Future<T> represents the result of an asynchronous operation and has two callbacks: one for when it completes and one for when it fails.

let future = Store.instance.fetchAccounts()
future.onComplete = { accounts in
  self.tableView.reloadData()
}
future.onError = { error in
  self.handleError(error)
}

Related: Swift Subarrays: Array and ArraySlice

You can only await on a method that returns a Future<T>. It is really just syntactic sugar: it takes everything that comes after the method call being awaited on and stuffs it in a closure, registering it as the callback of the Future<T> for the success case.

If the asynchronous operation completes successfully, the await keyword resolves into the result of the Future<T>. In other words, it strips the Future and just returns the T. Therefore, you can simply store the result of an await call in a variable. (Fun fact: you could also store the Future<T> in a variable and register callbacks yourself without using await).

If an error occurs during the asynchronous operation being awaited on, the error callback gets executed. And this is where Swift will really shine: leveraging Swift’s error handling syntax (try, catch, do, throws) to help support await.

First, any method marked as async implicitly returns a Future<T> and also throws.

// The following declaration:
func fetchAccounts() async -> Array<Account> {
  ...
}

// Is actually:

func fetchAccounts() throws -> Future<Array<Account>> {
  ...
}

When awaiting on an asynchronous method, you can choose to discard any error and just get back nil using await?:

let accounts = await? store.fetchAccounts()
/* do something with accounts, which may be nil */

You may also have explicit error handling, as await implicitly does a try:

do {
  let accounts = await store.fetchAccounts()
  /* do something with accounts, which may be nil */
} catch ... {
  ...
}

And you can still play with fire if you like:

let accounts = await! store.fetchAccounts()

Now, there is one thing to point out that is incorrect in my initial example of using await in viewWillAppear: you can’t use await in a method that isn’t asynchronous. If the method isn’t asynchronous, then the caller isn’t going to wait. So it doesn’t really do anything.

In this case, viewWillAppear is a synchronous method. The view controller lifecycle code that invokes viewWillAppear will not – and cannot – wait for an asynchronous operation to complete to continue. This would create some weird lifecycle issues for a view controller that present themselves as an unresponsive UI.

That’s okay, though. You don’t have to await on a Future<T> and you can still use callbacks, and those callbacks can be asynchronous and do some await for nested asynchronous calls.

 

If you really like Swift’s syntax, await appeals to you and you want to write web servers, check out Aqueduct.

 

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *