Promises in Swift

Written by Reinder de Vries on July 29 2020 in App Development, Swift

Promises in Swift

Promises in Swift simplify your asynchronous code. Instead of “callback hell” you create a concise chain of functions, and keep your code clean. Let’s find out how!

In this article you’ll learn how to use promises. We’ll use PromiseKit, a great library for iOS written in Swift. Promises are part of good app architecture.

Ready? Let’s go.

  1. The Problem Promises Solve
  2. Writing Asynchronous Code with Promises
  3. How to Make Promises with Swift
  4. Multiple Concurrent Tasks with “when”
  5. Async/Await in Swift
  6. Further Reading

The Problem Promises Solve

Why should you use promises? Let’s take a look at some Swift code:

Alamofire.request("http://jsonplaceholder.typicode.com/posts").responseData(completionHandler: { response in

    guard response.result.isSuccess else {
        print("Request error: \(response.result.error)")
        return
    }

    if let data = response.data
    {
        do {
            let json = try JSON(data: data)
            print(json)
        }
        catch {
            print("JSON error!")
        }
    }
})

What happens in this code?

  • An asynchronous HTTP request to http://jsonplaceholder.typicode.com/posts is made. When the response returns a closure is executed (often called a completion handler).
  • Within the closure, you check if the request was successful. If not, you print out an error and exit the closure.
  • Continuing, you check if response.data is not nil. If not, you try to parse the JSON from the response with a do-try-catch block.

The above code example is effective Swift code, but it’s neither elegant nor readable! What’s wrong with this code?

We’ve created a “pyramid” of brackets. Most of what happens in our code is indented on a new level. The most important part of the code is hidden away on the deepest level. It takes some reading to figure out all that this code example does, is print a bunch of JSON…

The code has two checks for errors. Three if you count if let data .... Imagine what happens when you extend that code with more functionality. Every time you clear the path forward – no HTTP errors, no JSON errors, no data errors – you indent one level. This makes your code progressively unreadable.

The example just makes one request. What if you want to make subsequent requests? Your code quickly becomes an entangled mess. And when you make requests in serial, that could have been parallelized, you’re writing underoptimized code. You’d end up with something like this:

Alamofire.request(···).responseData(completionHandler: { response in

    guard response.result.isSuccess else {
        ···
    }

    if let data = response.data {
        do {
            let json = try JSON(data: data)

            Alamofire.request(···).responseData( ···

                guard response.someError = nil else {
                    // Wait, whaat?    
                }
            })
        }
        catch {
            print("JSON error!")
        }
    }
})

You get the point. The code is hard to read, because you constantly have to step down into the next indent. The inner async HTTP request is obscured from view. You’re likely going to make a mistake. Chances are you’ll forget what the code does altogether. Not good!

Get hired as an iOS developer

Learn to build iOS 13 apps with Swift 5

Sign up for my iOS development course, and learn how to start your career as a professional iOS developer.

Writing Asynchronous Code with Promises

Let’s bring in promises! This is what the previous Swift code example now looks like:

Alamofire.request("http://jsonplaceholder.typicode.com/posts").responseData().then { data -> Void in

    let json = try JSON(data: data)
    print(json)

}.catch { error -> Void in
    print("Error: \(error)")
}

The code examples in this tutorial use trailing closure syntax. The function parameter name and parentheses can be omitted when the last function parameter is a closure.

Much better, right? This is what happens:

  1. The request is set up just like before. Instead of defining a completion handler, the response is now “chained” with a call to then. Chaining is typical when working with promises, and there are more syntax options like “then”.
  2. Within then, the data from the request is turned into a JSON object and printed out. Just like before, the JSON(data: data) initializer is marked with try because it can throw errors. However, it’s not wrapped in a do-catch block!
  3. The catch handler, also part of working with promises, handles the errors. It’s called for both HTTP errors from Alamofire and JSON errors with do-try-catch. Neat, right?

The example above is called a promise chain. You literally chain multiple promises together, which greatly improves your code readability.

Promises, and their cousins called Futures, get their name from the fact that they promise to return a result in the future. With promises, you’re providing a bunch of closures to a resolver, which fires them off one by one.

The resolver decides what to call next, and optionally catches errors regardless of where they happened in the chain. A key concept is that promises parallelize asynchronous requests and abstract away the asynchronous nature of the code.

Let’s deconstruct promises a bit further. This is a typical promise chain:

firstly {
    login()
}.then { creds in
    fetch(avatar: creds.user)
}.then { image in
    self.imageView = image
}.catch { error in
    print("Error: \(error)")
}.always {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
}

This is how it works:

  • The chain uses functions called firstly, then, catch and always. All of them are indented on the same level.
  • The firstly handler is executed immediately. Within that block a function login() is called. This function returns a promise, but more on that later!
  • The first then handler gets the returned result from the previous firstly handler. So the value of creds is the same as the return value of login(). The result is used to call fetch(avarar: creds.user). This fetch(...) function also returns a promise.
  • The second then handler uses the result of the previous handler, so the result of fetch(...). It assigns the returned image to an image view. See how the result of one promise is fed into the next?
  • The catch handler catches any errors that can occur in the preceding handlers. When logging in fails or an error is thrown the catch handler is invoked and the error is printed on screen.
  • Finally, the always handler. It’s always executed at the end of a chain regardless of errors. It is similar to Swift’s defer syntax, so you can use it to wrap up some tasks. In the example we hide iOS’s network indicator.

There are three key points to understanding promises.

  • First, in a promise chain, one task leads to another. They are executed procedurally, one by one, until the end of the chain is reached.
  • Second, with promises your functions return promises. Each handler in a chain returns a promise. Each then call waits for the promise of the previous handler. (You’re allowed to return nothing, too.)
  • Third, a promise represents the future value of an asynchronous task. In the example above, the login() function returns a promise that will represent the users credentials.

What’s so exciting about this? Don’t forget that this is all asynchronous code… Even though the login() and fetch() functions take an arbitrary amount of time to complete – you’re not sure when – you can code them as if they happen one after the other! Paradoxically, promises can synchronize asynchronous code.

How to Make Promises with Swift

OK, let’s dive in even further. What if you need to create your own promises?

PromiseKit has extensions for most iOS SDKs and popular libraries, so you can use them with promises out of the box. For instance, in the first example we used the PromiseKit extension for Alamofire.

So far, you’ve only used promises. You can also create your own promises. You do this when you’ve written your own networking code, lengthy custom task, or use an external API that you want to wrap with promises.

In most simple cases you can use PromiseKit’s wrap function, like this:

func download() -> Promise<String> {
    return PromiseKit.wrap(download)
}

In the above example, download is the asynchronous closure you want to use in your promise. Its result is of type String and PromiseKit automatically picks up on errors when it can.

PromiseKit has a number of generic wrap() functions that can automatically resolve asynchronous functions as promises.

You can also use PromiseKit’s resolver, like this:

func download() -> Promise<String> {
    return Promise { seal in
        asyncTask { result, error in
            if result != nil {
                seal.fulfill(result)
            } else {
                seal.reject(Error.invalidResult)
            }
        }
    }
}

In the above example, the asyncTask function is called with a closure as a completion handler. This is how you would typically code an asynchronous task, before using promises. The download() function returns a Promise, which is expected to return a value of type String.

PromiseKit needs to know when the asynchronous code has completed executing. In the example code you see one call to seal.fulfill() and one call to seal.reject(). This is the code that resolves the promise.

With fulfill() you indicate a positive result and with reject() you can indicate an error. Logically, fulfill() leads to the next then handler and reject() leads to the catch handler.

You rarely have to write your own promise resolving code. When in doubt, check the PromiseKit repository for extensions for your favourite SDKs or use the wrap function.

Multiple Concurrent Tasks with “when”

This is so much fun! One last thing… What if you want to use multiple concurrent asynchronous tasks? For instance, saving a photo and uploading it to your webserver.

A typical way is this:

savePhoto { path in 
    uploadPhoto { url in
        tasksFinished(path, url)
    }
}

That’s serial code, so it’s slower than it can be. First, you save the photo. When that’s done, you upload the photo. When that’s done, you call taskFinished().

Doing it in parallel, i.e. saving and uploading at the same time, is even more complicated than the above code.

With promises it’s easy:

firstly {
    when(fulfilled: savePhoto(), uploadPhoto())
}.then { path, url in
    // Do stuff
}

With the when function you define multiple promise handlers. The next then is only executed when these promise handlers resolve. When an error occurs in any of the promises, the catch handler is invoked – just as with any other promise.

So, in the above example code, both savePhoto() and uploadPhoto() are started at the same time. The then handler is executed when both tasks are completed, and it’s even provided the return values for those closures.

That’s nothing short of amazing!

Get hired as an iOS developer

Learn to build iOS 13 apps with Swift 5

Sign up for my iOS development course, and learn how to start your career as a professional iOS developer.

Async/Await in Swift

Promises are one of the best ways to deal with complex asynchronous code, but there’s an even better way: async and await. Promises and async/await are related. Unfortunately, async await syntax is not yet available in Swift.

Here’s how async and await would work in Swift:

func savePhoto() async -> Result? { ···
func uploadPhoto() async -> Result? { ···

func processPhoto() async -> Result? {
    let result1 = await savePhoto()
    let result2 = await uploadPhoto()
    ···
}

See what’s going on here? The 2 functions that do most of the work, i.e. saving and uploading a photo, are market with async. That’s because they’re asynchronous: their result isn’t returned immediately in sync.

The processPhoto() function includes calls to the 2 functions, and they’re marked with await. It’s like saying to Swift: wait for the result of this asynchronous function call. This won’t make the code synchronous; it’s all still async. You can code it, however, as if it’s all line-by-line synchronous code.

See the similarities with promises? Both approaches untangle async code. In fact, the code behind async/await is based on the same resolver mechanism as promises. The beauty of the async/await functionality is that you can abstract 99% of that away and make async code concise and insightful.

Right now, async/await won’t work in Swift. As of July 2020, discussions about async/await in Swift have flared up in the Swift forums. Chris Lattner and the Swift core team are committed to creating a better foundation for async/parallel programming with Swift – and it appears async/await is a promising candidate…

Async/await is the default way of working with asynchronous programming in JavaScript (ES-262). JavaScript is a popular language on the web, and it’s similar to Swift in many ways, so I imagine this is where the idea for async/await in Swift originally came from. There’s only so many ways to slice a cake…

Further Reading

So, promises. Now you know! See if you can use them in your next project, because they greatly reduce the complexity of asynchronous programming while improving readability.

You learned how to use promises, what problem they solve, how to create your own promises, and how to work with the various syntaxes like firstly, then, catch and when. Nice!

Make sure to check out PromiseKit and their Getting Started guide. The Common Patterns and FAQ wikis are informative, too. You can usually find a PromiseKit extensions by simply Googling for “promisekit [sdk or library name]”.

Want to learn more? Check out these resources:

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Start your iOS career
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy