Promises in Swift: Writing Cleaner Asynchronous Code Using PromiseKit

March 06, 2017
Written by
Sam Agnew
Twilion

Screen Shot 2017-03-06 at 5.11.06 PM

Writing asynchronous code in Swift can be painful. As many Node.JS developers are familiar with, you can easily run into problems like callback hell.

Although Swift and its developer ecosystem are still young, thanks to open source libraries like PromiseKit there is hope for Swift developers wanting to write cleaner code for handling asynchronous tasks.

Promises vs “Callback Hell”

To illustrate the difference in code structure, let’s use the Giphy API as an example. Here is how your code might look if you want to load a GIF from the Giphy API using the builts in language features and the Giphy class you will find in our project below:

giphy.fetchRandomGifUrl(forSearchQuery: "SNES") { imageUrlString, error in
  if error {
    print(error.localizedDescription)
    throw error
  }
  self.fetchImage(forImageUrl: imageUrlString) { imageData, error in
    if error {
      print(error.localizedDescription)
      throw error
    }
    self.attachImage(withImageData: imageData)
  }
}

It’s only a couple of function calls, but you can already see the code nesting and how each level requires its own separate error check.  It doesn’t take much imagination to see how code like this can quickly become difficult to read and understand in even more complex scenarios. Here is how you do the same thing if these functions are rewritten to use Promises:

giphy.fetchRandomGifUrl(forSearchQuery: "SNES").then { imageUrlString in
  self.fetchImage(forImageUrl: imageUrlString)
}.then { imageData in
  self.attachImage(withImageData: imageData)
}.catch { error in
  print(error.localizedDescription)
}

By having these functions return Promises instead of taking callbacks, we can use .then() to wait until one request is finished before starting the next one. And with .catch() we can catch an error whenever one happens. There is no need to repeat this error handling code.

This code becomes a lot easier to read, therefore it is easier to extend into more complex scenarios. To get that cleaner, more readable code we’ve got to do some work to wrap the request to the Giphy API in a promise. Thankfully PromiseKit makes this easy.

Let’s build an app to display random SNES (Super Nintendo Entertainment System) related GIFs that uses Promises to control the flow of our networking code. There’s nothing I love more than 16 bit video games and manageable networking code!

giphy.gif

Setting up our project

Get started by grabbing this starter project off of Github. It takes care of all the bare bones set up so we can just focus on the important stuff. Open your terminal, navigate to where you want this project to live and run the following command to clone this repository and check into the tutorial branch:

git clone git@github.com:sagnew/promises-swift-tutorial.git
cd promises-swift-tutorial/SNESGifs
git checkout tutorial

This app has several dependencies:

  • Alamofire for sending HTTP requests to the Giphy API.
  • SwifyJSON for dealing with the JSON response in a sane and reasonable way.
  • SwiftyGif for easily managing GIF data and displaying them in a UIImageView
  • PromiseKit for promises.

We’ll use CocoaPods to install these dependencies. First, install CocoaPods if you don’t have it:

sudo gem install cocoapods

Now install the dependencies:

pod install

CocoaPods links the dependencies of our project together by creating a new Xcode Workspace. To make sure XCode loads those dependencies open the workspace using the SNESGifs.xcworkspace file instead of the Xcode project file:

open SNESGifs.xcworkspace

We’re ready to begin our journey. Feel free to build the project to make sure everything compiles correctly.

giphy.gif

Writing a function that uses Promises

Let’s begin by making use of all the dependencies we just added. In GiphyManager.swift, add a function to fetch a URL for a random GIF from the Giphy API:

func fetchRandomGifUrl(forSearchQuery query: String) -> Promise<String> {
  let params = ["api_key": self.apiKey, "q": query, "limit": "\(self.imageLimit)"]

  // Return a Promise for the caller of this function to use.
  return Promise { fulfill, reject in

    // Inside the Promise, make an HTTP request to the Giphy API.
    Alamofire.request(self.giphyBaseURL, parameters: params)
      .responseJSON { response in
        if let result = response.result.value {
          let json = JSON(result)
          let randomNum = Int(arc4random_uniform(self.imageLimit))

          if let imageUrlString = json["data"][randomNum]["images"]["downsized"]["url"].string {
            fulfill(imageUrlString)
          } else {
            reject(response.error!)
          }

        }
    }
  }
}

Notice that we are returning a Promise as opposed to the alternative of taking a completion handler function. When the URL of a random image is successfully retrieved from the Giphy API, we pass it on by sending it as an argument to the fulfill function. If the request to Giphy fails, we pass that failure on by calling the reject function.

Once we have the URL for our image, we still need to download that image and load it into a UIImageView. Navigate over to ViewController.swift and add a function to fetch an image given a URL to that image:

func fetchImage(forImageUrl imageUrlString: String) -> Promise<Data> {

  // Return a Promise for the caller of this function to use.
  return Promise { fulfill, reject in

    // Make an HTTP request to download the image.
    Alamofire.request(imageUrlString).responseData { response in

      if let imageData = response.result.value {
        print("image downloaded")

        // Pass the image data to the next function.
        fulfill(imageData)
      } else {

        // Reject the Promise if something went wrong.
        reject(response.error!)
      }
    }
  }
}

This is very similar to what we did in the fetchRandomGifUrl function. We are making a request with Alamofire again and returning a Promise with a function that fulfills or rejects this Promise based on the results of our HTTP request.

Using Promises in this way is going to allow us to take a huge shortcut in the long run.

giphy.gif

To finish up the chain of functions we are going to use, we’ll write a quick function to attach the image to our UIImageView using SwiftyGif:

func attachImage(withImageData imageData: Data) -> Void {
  let image = UIImage(gifData: imageData)
  self.snesGifImageView.setGifImage(image)
}

Chaining Promises with .then()

So far what we are doing is:

  • Sending an HTTP request to the Giphy API to get a URL corresponding to a random GIF
  • Sending an HTTP request to that URL in order to download that GIF
  • Rendering that GIF to a UIImageView

Nesting network requests can get messy, but now we’re able to take advantage of the ability to chain Promises. Let’s put everything we’ve done so far together and take advantage of the functions we wrote that return Promises.

In viewDidLoad add the following code:

override func viewDidLoad() {
  super.viewDidLoad()
  let giphy = GiphyManager()

  giphy.fetchRandomGifUrl(forSearchQuery: "SNES").then { imageUrlString in
    self.fetchImage(forImageUrl: imageUrlString)
  }.then { imageData in
    self.attachImage(withImageData: imageData)
  }.catch { error in
    print(error)
  }

}

This reads much more nicely than nesting a bunch of completion handlers.

Now run the app and reap the rewards by seeing some sweet Super Nintendo GIFs!

*GIF of the app working*

Looking to the Future

Although the ecosystem might be young, you aren’t left totally hanging if you want an intuitive way to write asynchronous code without needing to rely on raw, bare bones Swift. There are also similar libraries such as BrightFutures and FutureKit. Perhaps one day, this will become a built in feature of the language like what is happening with Promises in JavaScript.

giphy.gif

I can’t wait to see what kind of awesome projects you build. Feel free to reach out for any questions: