Xcoding with Alfian

Software Development Videos & Tutorials

Writing Cleaner Asynchronous Code in Swift Using Promise Pattern

Alt text

What is the current state of writing asynchronous code in Swift 4?. The current mostly used pattern is to provide a callback closure that will be invoked when the task complete passing the result or an error. While this pattern works, it can be very hard to maintain and understand when we need to write multiple asynchronous tasks that also depends on the result of previous task.

struct UserProfile {}
func login(username: String, password: String, success: @escaping(String) -> Void, failure: @escaping(Error) -> Void)
func getProfile(token: String, success: @escaping(UserProfile) -> Void, failure: @escaping(Error) -> Void)

login(username: "alfianlosari", password: "123456", success: { token in
    getProfile(token: token, success: { (profile) in
        // Handle Get Profile success
    }, failure: { (error) in
      // Handle get profile error
    })
}) { (error) in
    // Handle login error
}

The asynchronous functions are nested inside one another that can lead to “Pyramid of Doom“ in our code. One of the approach the computer scientists discover to solve this kind of problem is by using Promise pattern that is used widely to write modern ES6 Javascript application.

Solving Callback Pyramid Of Doom problem with Promise

What is a Promise?. In a nutshell, promise is an object that represents the eventual completion (or failure) of an asynchronous task. The completion states of the operation can be in pending, failure, and completed:

  1. pending: unresolved task, task has not yet completed.
  2. fulfilled: resolved task with the result of the value
  3. rejected: resolved task with error.

The state of the promises object when it completes or fails are final and will never change.

The promise object can be attached to many observers that can be notified when the promise completes its task providing the resulting of value or when the promise fails providing the reason of the error using the error object. When it completes, the observers of the promise return another promise object or they can return void to end the subscription. When it returns another promise object, it can be used to perform multiple chaining pipelines of asynchronous task transforming value from each promise.

Other main features of Promises:

  1. Perform multiple asynchronous tasks in parallel and resolve when all the tasks completed successfully.
  2. Perform race of many asynchronous tasks and resolve the task that completed first with its value.
  3. The error handling of the promises is also pretty straightforward, with only one simple catch error handler that catches all the promises pipeline operations.
struct UserProfile {}
func getProfile(token: String) -> Promise<UserProfile> 
func login(username: String, password: String) -> Promise<String>

login(username: "alfianlosari", password: "123456")
  .then { getProfile(token: $0 ) }
  .then { print("Profile Retrieved: \($0)") }
  .catch { print("Error: \($0)")
}

Looking at the source code above, we can see the code look much more simpler and easier to maintain using Promise chaining pattern and single error catch handling if any of the promise fails.

Using Promise in iOS Application

There are many Cocoapods libraries that we can use to integrate Promise into our iOS application. One of my favorite is Promises library written by Google, it is written on Objective-C and is fully compatible to use with Swift. The performance of the library is also relatively fast and much more lightweight in binary size compared to another promise library such as PromiseKit. Simply add this line to your Podfile to integrate:

pod 'PromisesSwift'

Google Promises library main features taken from their GitHub repository:

Simple: The framework has intuitive APIs that are well documented making it painless to integrate into new or existing code.

Interoperable: Supports both Objective-C and Swift. Promises that are created in Objective-C can be used in Swift and vice versa.

Lightweight: Has minimum overhead that achieves similar performance to GCD and completion handlers.

Flexible: Observer blocks can be dispatched on any thread or custom queue.

Safe: All promises and observer blocks are captured by GCD which helps avoid potential retain cycles.

Tested: The framework has 100% test coverage.

Here is the Github repository for google/promises.

Example of Promise Code that Resolve a Fulfilled Value

We create a function that accepts a ClosedRange Int as the parameter, then output a Promise object that resolves with an integer value by performing sum of the range of numbers passed in the parameter.

To create a Promise object we just use the Promise initializer which is a generic initializer. We use Int as the return value, then tells the initializer to perform the execution inside a global dispatch background queue. We pass a closure to the initializer containing the task we want to perform, the closure provide 2 parameters, fulfill and reject. We use the the fulfill to resolve the promise with the value while we use the reject to resolve the promise with an error.

import Foundation
import Promises

func calculateSumOfIntegers(range: ClosedRange<Int>) -> Promise<Int> {
  return Promise<Int>(on: .global(qos: .background)) { (fullfill, reject) in
      let sum = range.reduce(0) { $0 + $1 }
      fullfill(sum)
  }
}

calculateSumOfIntegers(range: 1...100)
  .then { print($0) }
  .catch { print($0.localizedDescription) }

We invoke the function passing the range of number between 1 and 100 inclusive to generate the Promise object that we can observe by using the then keyword passing the closure that will be invoked when the promise resolve with the result to print the result. You can also return another Promise object inside the completion closure if you want to chain another Promise, in this case the promise chain is ended because we are not returning any Promise object.

Example of Promise Code that Rejects with Error

Here, we create a function that returns a Promise, inside the promise task closure we just reject the task passing the generated NSError object.

import Foundation
import Promises

func createSimplePromiseWithError() -> Promise<Any> {
  return Promise<Any>(on: .main) { (fullfill, reject) in
      reject(NSError(domain: "", code: 100, userInfo: [
        NSLocalizedDescriptionKey: "Promise Error Sample"
      ]))
  }
}

createSimplePromiseWithError()
  .then { (_) in }
  .catch { (error) in
    print(error.localizedDescription)
}

We invoke the function to generate the Promise, then observe it. The catch keyword at the bottom will always catch the error in the Promise chain.

Writing Promise to Fetch Data From Network and Decode with Chaining

In this example we will create 2 promises:

  1. getSWAPIFilmData Promise: Fetch Data of films from SWAPI server, then resolve the promise with Data.
  2. decodeSWAPIFilmDataToFilms: Accepts Data as parameter, then decode the Data into JSON Dictionary and generate array of struct Film object using the JSON. The Promise resolve with array of Films struct.

To execute the tasks we first observe the getSWAPIFilmData promise, then when it resolves passing the data we just use the data to generate the decodeSWAPIFilmDataToFilms promise and return it to continue the chain. At last when the decode resolves, we just print the result which is the list of Films.

import Foundation
import Promises

getSWAPIFilmData()
  .then { decodeSWAPIFilmDataToFilms(data: $0) }
  .then { print($0) }
  .catch { print($0.localizedDescription) }

func getSWAPIFilmData() -> Promise<Data> {
  return Promise<Data>(on: .global(qos: .background), { (fullfill, reject) in
    let urlSession = URLSession.shared
    let filmURL = URL(string: "https://swapi.co/api/films")!

    urlSession.dataTask(with: filmURL) { (data, response, error) in
      if let error = error {
        reject(error)
        return
      }

      guard let data = data else {
        let error = NSError(domain: "", code: 100, userInfo: nil)
        reject(error)
        return
      }

      fullfill(data)
    }.resume()
  })
}

func decodeSWAPIFilmDataToFilms(data: Data) -> Promise<[Film]> {
  return Promise<[Film]>(on: .global(qos: .background), { (fullfill, reject) in
    do {
      let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
      guard let jsonDictionary = jsonObject as? [String: Any], let result = jsonDictionary["results"] as? [[String: Any]] else {
          reject(NSError(domain: "", code: 100, userInfo: nil))
          return
      }
      let films = result.map { Film(jsonDictionary: $0) }.compactMap { $0 }
      fullfill(films)
    } catch {
      reject(error)
    }
  })
}


struct Film {
   
  var director: String
  var episodeId: Int
  var openingCrawl: String
  var producer: String
  var releaseDate: Date
  var title: String

  static let dateFormatter: DateFormatter = {
    let df = DateFormatter()
    df.dateFormat = "YYYY-MM-dd"
    return df
  }()
}

extension Film {

  init?(jsonDictionary: [String: Any]) {
      guard let director = jsonDictionary["director"] as? String,
        let episodeId = jsonDictionary["episode_id"] as? Int,
        let openingCrawl = jsonDictionary["opening_crawl"] as? String,
        let producer = jsonDictionary["producer"] as? String,
        let releaseDate = jsonDictionary["release_date"] as? String,
        let title = jsonDictionary["title"] as? String
          else {
            return nil
      }

    self.init(director: director, episodeId: episodeId, openingCrawl: openingCrawl, producer: producer, releaseDate: Film.dateFormatter.date(from: releaseDate) ?? Date(timeIntervalSince1970: 0), title: title)
  }
}

Writing Promise to Perform Multiple Asynchronous Tasks in Parallel

In this example, we write 3 functions that generate Promises that will be fulfilled with the default value. we use the Promises all function that accepts variadic parameters for Promise object in which we pass our 3 Promise object. All the promises operation will be performed in parallel, then only resolves when all the promises resolve successfully otherwise error will be thrown if one the promise get rejected.

import Foundation
import Promises

all(performJobOne(),performJobTwo(),performJobThree())
  .then { (results) in
    print(results.0)
    print(results.1)
    print(results.2) }
  .catch { $0.localizedDescription }

func performJobOne() -> Promise<String> {
  return Promise<String> { "Job One Finished" }
}

func performJobTwo() -> Promise<Bool> {
  return Promise<Bool> { true }
}

func performJobThree() -> Promise<Int> {
  return Promise<Int> { 10 }
}

Conclusion

Using Promise to write asynchronous code in Swift is one of the solution to avoid the nested callback pyramid of doom in our source code. The Swift itself is an evolving language and there is already a proposal on how to write better asynchronous code from Chris Lattner (creator of Swift) using Async/Await pattern. Async/Await make the synchronous code looks like run serially when writing the code that makes it easier to understand and maintain. It is already been used widely in the latest Javascript ECMA standard and C#.