Swift 4 and PromiseKit

by
Tags:
Category:

Having spent years in AngularJS, using promises to wrap asynchronous calls became an almost daily occurrence. I loved the way asynchronous code could be quickly and easily encapsulated with a quick promise using the $q library. When I made the jump to iOS and Swift, I was left wondering how I could continue working with promises and asynchronous code. Enter PromiseKit.

While PromiseKit isn’t as intuitive as $q, it is every bit as capable and powerful but with its own quirks.

The Basics

Promises intend to solve the issues when invoking asynchronous code such as calling an API on a server.  The http URL is invoked and, sometime later, a response is returned, whether success or failure, or the call hangs.  When invoking asynchronous code without using promises,  a callback function is passed along that will eventually be invoked with the response.  This is fairly straight forward until several asynchronous calls need to be made sequentially or in parallel.  Clean separation of success and error code is difficult as are other common tasks such as timing out and waiting for multiple calls to complete before proceeding.  Promises solve these common issues and provide useful shortcuts for others.

The basics of handling a promise are providing success and error handlers which are invoked if the asynchronous code succeeds, i.e. is fulfilled, or errors, i.e. is rejected.  A promise gets resolved when it is either fulfilled or rejected.  A promise that has not resolved is pending.  Optionally, a finally block can be provided which executes regardless of whether the promise was fulfilled or rejected.

Wrapping Asynchronous APIs

Libraries that don’t utilize promises can be made to do so by wrapping the asynchronous code with a promise.  Simply create a new promise that is in the pending state with the correct return type and resolve it based on the outcome of the asynchronous code.  Allocating a new promise returns a tuple containing both a pending promise and a resolver.  The new promise should be returned to the code invoking the asynchronous code and the resolver should be used to eventually resolve it.  A promise can only be resolved once and, once resolved, remains so.

     
import UIKit
import Alamofire
import PromiseKit

struct User: Codable{
    let id: Int
    let name: String
    let username: String
    let email: String
}

struct Post: Codable{
    let id: Int
    let userId: Int
    let title: String
    let body: String
}

enum ApplicationError: Error {
    case noUsers
    case usersCouldNotBeParsed
    case postsCouldNotBeParsed
}

func getAllUsers() -> Promise{
    let (promise, resolver) = Promise.pending()
    
    Alamofire.request(usersUrlString).responseJSON{ response in
        if let error = response.error{
            resolver.reject(error)
        }
        
        if let data = response.data {
            do{
                let users = try self.decoder.decode([User].self, from: data)
                resolver.fulfill(users)
            }catch{
                resolver.reject(ApplicationError.usersCouldNotBeParsed)
            }
        }else{
            resolver.reject(ApplicationError.noUsers)
        }
    }
    
    return promise
}

Then vs. Done

The code receiving the newly created promise provides the handlers for the fulfilled and rejected cases.  The first stumbling block for me was whether to use then or done as my fulfilled promise handler. Using the incorrect function results in some non-intuitive compiler errors about type mismatches. then is to be used when your promise handler returns a promise itself and done when it doesn’t. Essentially, use then when chaining promises and done when the chain ends

Example of using done:

         
        getAllUsers()
            .done{ users -> Void in
                print("Promise users: \(users)")
            }.catch{error in
                print("Something went wrong: \(error)")
        }

Example of using then and done:

        
func getPosts(for userId:Int) -> Promise{
    let (promise, resolver) = Promise.pending()
    
    let parameters = ["userId": userId]
    Alamofire.request(postsUrlString, parameters: parameters).responseJSON{ response in
        if let error = response.error{
            resolver.reject(error)
        }
        
        if let data = response.data {
            do{
                let posts = try self.decoder.decode([Post].self, from: data)
                resolver.fulfill(posts)
            }catch{
                resolver.reject(ApplicationError.postsCouldNotBeParsed)
            }
        }else{
            resolver.fulfill([Post]())
        }
    }
    
    return promise
}
      
getAllUsers()
    .then{ users -> Promise in
        print("Promise users: \(users)")
        
        return self.getPosts(for: users[4].id)
    }.done{ posts -> Void in
        print("Posts for user: \(posts)")
    }.catch{error in
        print("Something went wrong: \(error)")
}

Map

What to do when the promise returns type X but you need it to return type Y?  JavaScript, not being typesafe, allows a mix and match of promise return types and it’s up to the developer to know what to expect.  This is impossible with Swift’s strict type checking. Use the map extension function instead. Return a non-promise type from the promise handler and PromiseKit automatically wraps it in an appropriately typed promise.

Example:

    
func getUserName(for userId: Int) -> Promise{
    return getAllUsers()
        .map{ users -> String in
            let user = users.first( where: {$0.id == userId} )
            
            if let user = user {
                return user.username
            }else{
                return "Not found"
            }
        }
}

Delay

What to do when the server you’re communicating with isn’t keeping up with a chain of computationally-intensive calls or when a pause is needed when changing an element in the UI?  Use the after convenience function.

Example:

         
after(seconds: 1).done{
    print("1 second has passed")
}

Timeout

What to do when that pesky server call hangs and there’s no hope of getting a reply?  Use a delay with the race convenience function. The race convenience function invokes the promise handler as soon as any one of the provided promises either returns or rejects.

Example:

 
let timeout = after(seconds: 10)
let getAllUsersPromise = getAllUsers()
race(getAllUsersPromise.asVoid(), timeout.asVoid())
    .done {
        if timeout.isResolved {
            print("getAllUsers() timed out")
        }else if let users = getAllUsersPromise.value{
            print("Promise users: \(users)")
        }
    }.catch{error in
        print("Something went wrong: \(error)")
    }

There are a few extras here that warrant explanation. First, there is asVoid().  All promises passed to race must return the same type so, when this isn’t possible normally, the asVoid() method changes the return type to Promise<Void>. Then in the promise handler, check the isResolved property of the timeout promise to see if the time specified has passed before the getAllUsersPromise could complete. Otherwise, retrieve the result of the getAllUsersPromise via its value property.

When

What to do when you need to make several calls in parallel? Use the when convenience function. PromiseKit’s when behaves differently than its $q cousin which returns an immediately fulfilled promise from a non-promise.  PromiseKit’s when waits for all provided promises to fulfill or for the first one to reject. Please note that, if one promise rejects, the other promises may still fulfill or reject but these will be ignored.

Example:

 
let getAllUsersPromise = getAllUsers()
let getAllPostsPromise = getAllPosts()
when(fulfilled: [getAllPostsPromise.asVoid(), getAllUsersPromise.asVoid()])
    .done{ _ in
        if let users = getAllUsersPromise.value, let posts = getAllPostsPromise.value {
            print("Promise users: \(users)")
            print("Promise posts: \(posts)")
        }
    }.catch{ error in
        if getAllPostsPromise.isRejected {
            print("getAllUsers() errored: \(error)")
        }else{
            print("getAllPosts() errored: \(error)")
        }
    }

Check the isRejected of each promise to find the one that failed, if necessary.

What to do when you need to wait for all of the promises to resolve regardless if any have rejected? In that case, when also has you covered. This variation waits for all provided promises to either reject or fulfill and then invokes the promise handler.

Example:

 
let getAllUsersPromise = getAllUsers()
let getAllPostsPromise = getAllPosts()
when(resolved: [getAllPostsPromise.asVoid(), getAllUsersPromise.asVoid()])
.done{ results in
    var anyPromiseFailed = false
    
    results.forEach{ result in
        switch result {
        case .rejected(let error):
            print("Action partially failed: \(error)")
            anyPromiseFailed = true
        default: break
        }
    }
    
    if anyPromiseFailed {
        //handle the error
    }else if let users = getAllUsersPromise.value,
        let posts = getAllPostsPromise.value {
        print("Promise users: \(users)")
        print("Promise posts: \(posts)")
    }
}

The return value of when is an array of each promise’s results in the order in which they were passed. when(resolved:) never errors since the result of each promise, whether fulfilled or rejected, is passed to the promise handler.

Conclusion

I hope this helps you delve deeper into the PromiseKit toolbox and elucidates the most common use cases you’ll encounter. Please leave any questions, comments, or quemments below.

In order to try out PromiseKit yourself with a generic JSON server, visit JSONPlaceholder.