Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4399

Getting Started with PromiseKit

$
0
0

PromiseKit-featureAsynchronous programming can be a real pain in the lemon. Unless you’re extremely careful, it can easily result in humongous delegates, messy completion handlers and long nights debugging code! Lucky for you, there’s a better way: promises. Promises tame asynchronicity by letting you write code as a series of actions based on events. This works especially well for actions that must occur in a certain order. In this PromiseKit tutorial, you’ll learn how to use the third party PromiseKit to clean up your asynchronous code, and your sanity.

Typically, iOS programming involves many delegates and callbacks. You’ve likely seen a lot of code like this:

- Y manages X.
- Tell Y to get X.
- Y notifies its delegate when X is available.

Promises attempt to simplify this mess to look more like this:

When X is available, do Y.

Doesn’t that look delightful? Promises also let you separate error handling and success code, which makes it easier to write clean code that handles many different conditions. They work great for complicated, multistep workflows like logging into web services, making authenticated SDK calls, processing and displaying images and more!

Promises are becoming more common and there are multiple solutions, but in this tutorial, you’ll learn about promises using a popular, third-party Swift library called PromiseKit.

Getting Started

The project for this tutorial, WeatherOrNot, is a simple current weather application. It uses OpenWeatherMap for its weather API. The patterns and concepts for accessing this API are translatable to any other web service.

Download the starter project here. PromiseKit is distributed via CocoaPods, but the download already includes the pod. If you haven’t used CocoaPods before and would like to learn about it, you can read our tutorial on it. Other than noting PromiseKit has been installed via CocoaPods, however, this tutorial doesn’t require any other knowledge about CocoaPods.

Open PromiseKitTutorial.xcworkspace, and you’ll see the project is very simple. It only has five .swift files:

  • AppDelegate.swift: This is just the auto-generated app delegate file.
  • BrokenPromise.swift: This file creates a placeholder promise that is used to stub some parts of the starter project.
  • WeatherViewController.swift: This view controller handles all of the user interaction. This will be the main consumer of the promises.
  • LocationHelper.swift: This is a helper file that wraps CoreLocation.
  • WeatherHelper.swift: This helper file wraps the weather data provider.

The OpenWeatherMap API

Speaking of weather data, WeatherOrNot uses OpenWeatherMap to source weather information. Like most third party APIs, this requires a developer API key to access the service. Don’t worry, there is a free tier that is more than generous enough to complete this tutorial.

You’ll need to get an API key to run the app. Get one at http://openweathermap.org/appid. Once you complete the registration, you can find your API key at https://home.openweathermap.org/api_keys.

OpenWeatherMap API Key

Copy that key and paste it into the appID constant at the top of WeatherHelper.swift.

Try it Out

Build and run the app. If all has gone well, you should see the current weather in Athens.

Well, maybe… the app actually has a bug (you’ll fix it soon!), so the UI may be a bit slow to show.

1_build_and_run

Understanding Promises

You already know what a “promise” is in everyday life. For example, you can promise yourself a cold drink when you complete this tutorial. This statement contains an action (“have a cold drink”) to take place in the future when an action is complete (“you finish this tutorial”). Programming promises are similar in that there is an expectation something will be done in the future when some data is delivered.

Promises are about managing asynchronicity. Unlike traditional methods, such as callbacks via completions or selectors, promises can be easily chained together, so a sequence of asynchronous actions can be expressed. Promises are also like operations in that they have an execution lifecycle and can be cancelled.

A PromiseKit Promise executes a code block that is fulfilled with a value. Upon fulfillment, its then block is executed. If that block returns a promise, then that will be executed, fulfilled with a value and so on. If there is an error along the way, an optional catch block will be executed instead.

For example, the colloquial promise above rephrased as a PromiseKit Promise looks like:

doThisTutorial().then { haveAColdOne() }.catch { postToForum(error) }

What PromiseKit…Promises

PromiseKit is a swift implementation of promises. While it’s not the only one, it’s one of the most popular. In addition to providing block-based structures for constructing promises, PromiseKit also includes wrappers for many of the common iOS SDK classes and easy error handling.

To see a promise in action, take a look at the function in BrokenPromise.swift():

func BrokenPromise<T>(method: String = #function) -> Promise<T> {
  return Promise<T>() { fulfill, reject in
    let err = NSError(domain: "PromiseKitTutorial", code: 0, userInfo: [NSLocalizedDescriptionKey: "'\(method)' has not been implemented yet."])
    reject(err)
  }
}

This returns a new generic Promise, which is the primary class provided by PromiseKit. Its constructor takes a simple execution block with two parameters:

  • fulfill: A function to call when the desired value is ready to fulfill the promise
  • reject: A function to call if there is an error

For BrokenPromise, the code always returns an error. This helper object is used to indicate that there is still work to do as you flesh out the app.

Making Promises

Accessing a remote server is one of the most common asynchronous tasks, and a straightforward network call is a good place to start.

Take a look at getWeatherTheOldFashionedWay(latitude:longitude:completion:) in WeatherHelper.swift. This method fetches weather data given a latitude, longitude and completion closure.

However, the completion closure is called on either success or failure. This results in a complicated closure since you’ll need code for both error handling and success within it.

Most egregiously, the data task completion is handled on a background thread, so this results in (accidentally :cough:) updating the UI in the background! :[

Can promises help here? Of course!

Add the following right after getWeatherTheOldFashionedWay(latitude:longitude:completion:):

func getWeather(latitude: Double, longitude: Double) -> Promise<Weather> {
  return Promise { fulfill, reject in
    let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)" +
                     "&appid=\(appID)"
    let url = URL(string: urlString)!
    let request = URLRequest(url: url)
 
    let session = URLSession.shared
    let dataTask = session.dataTask(with: request) { data, response, error in
      if let data = data,
        let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any],
        let result = Weather(jsonDictionary: json) {
        fulfill(result)
 
      } else if let error = error {
        reject(error)
 
      } else {
        let error = NSError(domain: "PromiseKitTutorial", code: 0,
                            userInfo: [NSLocalizedDescriptionKey: "Unknown error"])
        reject(error)
      }
    }
    dataTask.resume()
  }
}

This method also uses URLSession like getWeatherTheOldFashionedWay does, but instead of taking a completion closure, the networking is wrapped in a Promise.

In the dataTask‘s completion handler, if the data is successfully returned, a Weather object is created from the deserialized JSON. Here the fulfill function is called with that object, completing the promise.

If there is an error with the network request, that error object is passed to reject.

Else, if there’s neither JSON data nor an error, then a stub error is passed to reject as a promise rejection requires an error object.

Next, in WeatherViewController.swift replace handleLocation(city:state:latitude:longitude:) with the following:

func handleLocation(city: String?, state: String?,
                    latitude: CLLocationDegrees, longitude: CLLocationDegrees) {
  if let city = city, let state = state {
    self.placeLabel.text = "\(city), \(state)"
  }
 
  weatherAPI.getWeather(latitude: latitude, longitude: longitude).then { weather -> Void in
    self.updateUIWithWeather(weather: weather)
 
    }.catch { error in
      self.tempLabel.text = "--"
      self.conditionLabel.text = error.localizedDescription
      self.conditionLabel.textColor = errorColor
  }
}

Nice, using a promise is as simple as supplying then and catch blocks!

This new implementation of handleLocation is superior to the previous one. First, completion handling is now broken into two easy-to-read closures: then for success and catch for errors. Second, by default PromiseKit executes these closures on the main thread, so there’s no change of accidentally updating the UI on a background thread.

PromiseKit Wrappers

This is pretty good but PromiseKit can do better. In addition to the code for Promise, PromiseKit also includes extensions for common iOS SDK methods that can be expressed as promises. For example, the URLSession data task method returns a promise instead of using a completion block.

Replace the new getWeather(latitude:longitude:) with the following code:

func getWeather(latitude: Double, longitude: Double) -> Promise<Weather> {
  return Promise { fulfill, reject in
    let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=" +
                    "\(latitude)&lon=\(longitude)&appid=\(appID)"
    let url = URL(string: urlString)!
    let request = URLRequest(url: url)
 
    let session = URLSession.shared
 
    // 1
    let dataPromise: URLDataPromise = session.dataTask(with: request)
 
    // 2
    _ = dataPromise.asDictionary().then { dictionary -> Void in
 
      // 3
      guard let result = Weather(jsonDictionary: dictionary as! [String : Any]) else {
        let error = NSError(domain: "PromiseKitTutorial", code: 0,
                            userInfo: [NSLocalizedDescriptionKey: "Unknown error"])
        reject(error)
        return
      }
 
      fulfill(result)
 
      // 4
      }.catch(execute: reject)
  }
}

See how easy it is to use PromiseKit wrappers? Here’s the breakdown:

  1. PromiseKit provides a new overload of URLSession.dataTask(with:) that returns a URLDataPromise, which is just a specialized Promise. Note the data promise automatically starts its underlying data task.
  2. The returned dataPromise has a convenience method asDictionary(), which handles deserializing the JSON for you, significantly reducing the amount of code!
  3. Since the dictionary is already parsed, you use it to create a result. You do this using guard let to ensure a Weather object can be created from the dictionary. If not, you create an error and call reject, similar to before. Otherwise, you call fulfill with the result.
  4. Along the way, the network request could fail, or the resulting JSON deserializing could fail. Before both conditions had to be checked individually. Here, a single catch block forwards any errors on through the fail closure.

In this function, two promises are chained together. The first is the data promise, which returns a Data from the URL request. The second is asDictionary(), which takes the data and turns it into a dictionary.

Adding Location

Now that the networking is bullet-proofed, take a look at the location functionality. Unless you’re lucky enough to be visiting Athens, the app isn’t giving you particularly relevant data. Change that to use the device’s current location.

In WeatherViewController.swift replace updateWithCurrentLocation() with the following:

private func updateWithCurrentLocation() {
  // 1
  _ = locationHelper.getLocation().then { placemark in
    self.handleLocation(placemark: placemark)
    }.catch { error in
      self.tempLabel.text = "--"
      self.placeLabel.text = "--"
      switch error {
 
      // 2
      case is CLError where (error as! CLError).code == CLError.Code.denied:
        self.conditionLabel.text = "Enable Location Permissions in Settings"
        self.conditionLabel.textColor = UIColor.white
      default:
        self.conditionLabel.text = error.localizedDescription
        self.conditionLabel.textColor = errorColor
      }
  }
}
  1. This uses a helper class to work with Core Location. You’ll be implementing it in a moment. The result of getLocation() is a promise to get a placemark for the current location.
  2. This catch block demonstrates how different errors are handled within a single catch block. Here a simple switch is able to provide a different message when the user hasn’t granted location privileges versus other types of errors.

Next, in LocationHelper.swift replace getLocation() with this:

func getLocation() -> Promise<CLPlacemark> {
  // 1
  return CLLocationManager.promise().then { location in
 
    // 2
    return self.coder.reverseGeocode(location: location)
  }
}

This takes advantage of two PromiseKit concepts already discussed: SDK wrapping and chaining.

  1. CLLocationManager.promise() returns a promise of the current location.
  2. Once the current location is obtained, it’s passed on to CLGeocoder.reverseGeocode(location:), which also returns a promise to provide the reverse-coded location.

With promises, two different asynchronous actions are linked in three lines of code! No explicit error handling is required because that all gets taken care of in the catch block of the caller.

Build and run. After accepting the location permissions, the current temperature for your (simulated) location is shown. Voilà!

Build and run, with location

Searching for an Arbitrary Location

That’s all well and good, but what if a user wants to know the temperature somewhere else?

In WeatherViewController.swift replace textFieldShouldReturn(_:) with the following (ignore the compiler error for now about the missing method):

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
 
  textField.resignFirstResponder()
 
  guard let text = textField.text else { return true }
  _ = locationHelper.searchForPlacemark(text: text).then { placemark -> Void in
    self.handleLocation(placemark: placemark)
  }
  return true
}

This uses the same pattern as all the other promises: go off and find the placemark and when that’s done, update the UI.

Next, add the following to LocationHelper.swift:

func searchForPlacemark(text: String) -> Promise<CLPlacemark> {
  return CLGeocoder().geocode(text)
}

It’s that simple! PromiseKit already has an extension for CLGeocoder to find a placemark that returns a promise with a placemark.

Build and run. This time, enter a city name in the search field at the top and tap Return. This should then go and find the weather for the best match for that name.

Weather, by search

Threading

One thing so far has been taken for granted is that all the then blocks have been executed on the main thread. This is a great feature since most of the work done in the view controller has been to update the UI. Sometimes, however, long-running tasks are best handled on a background thread, so as not to tie up the app.

You’ll next add a weather icon from OpenWeatherMap to illustrate the current weather conditions.

Add the following method to WeatherHelper right after getWeather(latitude:longitude:):

func getIcon(named iconName: String) -> Promise<UIImage> {
  return Promise { fulfill, fail in
    let urlString = "http://openweathermap.org/img/w/\(iconName).png"
    let url = URL(string: urlString)!
    let request = URLRequest(url: url)
 
    let session = URLSession.shared
    let dataPromise: URLDataPromise = session.dataTask(with: request)
    let backgroundQ = DispatchQueue.global(qos: .background)
    _ = dataPromise.then(on: backgroundQ) { data -> Void in
      let image = UIImage(data: data)!
      fulfill(image)
      }.catch(execute: fail)
  }
}

Here, building a UIImage from the loaded Data is handled on a background queue by supplying an optional on parameter to then(on:execute:). PromiseKit then handles the heavy lifting of performing all the necessary dispatches.

Now the promise is fulfilled on the background queue, so the caller will need to make sure the UI is updated on the main queue.

Back in WeatherViewController.swift, replace the call to getWeather(latitude:longitude:) inside handleLocation(city:state:latitude:longitude:) with this:

// 1
weatherAPI.getWeather(latitude: latitude, longitude: longitude).then { weather -> Promise<UIImage> in
  self.updateUIWithWeather(weather: weather)
 
  // 2
  return self.weatherAPI.getIcon(named: weather.iconName)
 
  // 3
  }.then(on: DispatchQueue.main) { icon -> Void in
    self.iconImageView.image = icon
 
  }.catch { error in
    self.tempLabel.text = "--"
    self.conditionLabel.text = error.localizedDescription
    self.conditionLabel.textColor = errorColor
}

There are three subtle changes to this call:

  1. First, the getWeather(latitude:longitude:) then block is changed to return a Promise instead of Void. This means that when the getWeather promise is complete, there will be a new promise returned.
  2. The just added getIcon method creates a new promise to… get the icon.
  3. A new then block is added to the chain, which will be executed on the main queue when the getIcon promise is fulfilled.

Thereby, promises can be chained into a sequence of serially executing steps. After one promise fulfills, the next will be executed, and so on until the final then or an error occurs and the catch is invoked. The two big advantages of this approach over nested completions are:

  1. The promises are composed in a single chain, which is easy to read and maintain. Each then block has its own context, keeping logic and state from bleeding into each other. A column of blocks is easier to read without an ever-deepening indent.
  2. All the error handling is done in one spot. For example, in a complicated workflow like a user login, a single retry error dialog can be displayed if any step fails.

Build and run, and image icons should now load!

Weather with icon

Wrapping in a Promise

What about using existing code, SDKs, or third party libraries that don’t have PromiseKit support built in? Well, for that PromiseKit comes with a promise wrapper.

Take, for instance, this application. Since there is a limited number of weather conditions, it’s not necessary to fetch the condition icon from the web every time; it’s inefficient and potentially costly.

In WeatherHelper.swift there are already helper functions for saving and loading an image file from a local caches directory. These functions perform the file I/O on a background thread, and use an asynchronous completion block when the operation is finished. This is a common pattern, so PromiseKit has a way of handling it.

Replace getIcon(named:) from WeatherHelper with the following (again, ignore the compiler error about the missing method for now):

func getIcon(named iconName: String) -> Promise<UIImage> {
 
  // 1
  return wrap {
    // 2
    getFile(named: iconName, completion: $0)
 
    } .then { image in
      if image == nil {
        // 3
        return self.getIconFromNetwork(named: iconName)
 
      } else {
        // 4
        return Promise(value: image!)
      }
  }
}

Here’s how it’s used:

  1. wrap(body:) takes any function that follows one of several completion handler idioms and wraps it in a promise.
  2. getFile(named: completion:) has a completion parameter of @escaping (UIImage?) -> Void, which becomes a Promise. It’s in wrap‘s body block where the original function is called, passing in the completion parameter.
  3. Here, if the icon was not located locally, a promise to fetch it over the network is returned.
  4. But if the image is available, it is returned in a value promise.

This is a new way of using promises. If a promise created with a value is already fulfilled, it’s then block will be immediately called. In such, if the image is already loaded and ready to go, it can be returned right away. This pattern is how you can create a promise that can either do something asynchronously (like load from the network) or synchronously (like use an in-memory value). This is useful when you have locally cached value, such as an image here.

To make this work, you’ll have to add the images to the cache when they come in. Add the following right below the previous method:

func getIconFromNetwork(named iconName: String) -> Promise<UIImage> {
  let urlString = "http://openweathermap.org/img/w/\(iconName).png"
  let url = URL(string: urlString)!
  let request = URLRequest(url: url)
 
  let session = URLSession.shared
  let dataPromise: URLDataPromise = session.dataTask(with: request)
  return dataPromise.then(on: DispatchQueue.global(qos: .background)) { data -> Promise<UIImage> in
    return firstly { Void in
      return wrap { self.saveFile(named: iconName, data: data, completion: $0)}
      }.then { Void -> Promise<UIImage> in
        let image = UIImage(data: data)!
        return Promise(value: image)
    }
  }
}

This is similar to the previous getIcon(named:) except that in the dataPromise‘s then block, there is a call to saveFile that is wrapped just like the use ofgetFile.

This uses a new construct, firstly. firstly is functional sugar that simply executes its promise. It’s not really doing anything other than adding a layer of indirection for readability. Since the call to saveFile is a just a side effect of loading the icon, using firstly here enforces a little bit of ordering so that we can be confident that this promise.

All in all, here’s what happens the first time you request an icon:

  1. First, a URL request is loaded.
  2. Once that is done, the data is first saved to a file
  3. After saving, the data is turned into an image and sent down the chain.

If you build and run now, you shouldn’t see any difference in app functionality, but you can check the filesystem to see that the images are saved. To do that, search the console output for the term Saved image to:. This will show the URL of the new file, which you can use to find its location on disk.

Cached Images

Ensuring Actions

Looking at the PromiseKit syntax, you might have asked: if there is a then and a catch, is there a way to share code and make sure an action is always taken (like a cleanup task), regardless of success or failure? Well there is: it’s called always.

In WeatherViewController.swift update handleLocation(city:state:latitude:longitude:) to show a network activity indicator in the status bar while the weather is being loaded from the server.

Insert the following line before the call to weatherAPI.getWeather...:

UIApplication.shared.isNetworkActivityIndicatorVisible = true

Then, to the end of the catch block add the following:

.always {
  UIApplication.shared.isNetworkActivityIndicatorVisible = false
}

Then, you might need to assign the whole expression to _ in order to silence an unused result warning.

This is the canonical example of when to use always. Regardless if the weather is completely loaded or if there is an error, the network activity will be complete, so the activity indicator should always be dismissed. Similarly, this can be used to close sockets, database connections, or disconnect from hardware services.

Timers

One special case is a promise that fulfills, not when some data is ready, but after a certain time interval. Currently, after the weather is loaded, it is never refreshed. Change that to update the weather hourly.

In updateWithCurrentLocation(), add the following code to the end of the method:

_ = after(interval: oneHour).then {
  self.updateWithCurrentLocation()
}

.after(interval:) creates a promise that is fulfilled after the specified interval passes. Unfortunately, this is a one-shot timer. To do the update every hour, it was made recursive onupdateWithCurrentLocation().

Much, Much Later

Parallel Promises

So far, all the promises discussed have either been standalone or chained together in a sequence. PromiseKit also provides functionality for wrangling multiple promises fulfilling in parallel. There are three functions for waiting for multiple promises. The first, race returns a promise that is fulfilled when the first of a group of promises is fulfilled. In essence, the first one completed is the winner.

The other two functions are when and join. Those fulfill after all the specified promises are fulfilled. Where these two differ is in the rejected case. join always waits for all the promises to complete before rejected if one of them rejects, but when(fulfilled:) rejects as soon as any one of the promises rejects. There’s also a when(resolved:) that waits for all the promises to complete, but always calls the then block and never the catch.

Note: For all of these grouping functions, all the individual promises will continue until they fulfill or reject, regardless of the behavior of the combining function. For example, if three promises are used in a race, the race‘s then block will be called after the first promise to complete. However, the other two unfilled promises keep executing until they too resolve.

Take the contrived example of showing the weather in a “random” city. Since the user doesn’t care what city it will show, the app can try to fetch weather for multiple cities, but just handle the first one to complete. This gives the illusion of randomness.

Replace showRandomWeather(_:) with the following:

@IBAction func showRandomWeather(_ sender: AnyObject) {
  let weatherPromises = randomCities.map { weatherAPI.getWeather(latitude: $0.2, longitude: $0.3) }
  _ = race(promises: weatherPromises).then { weather -> Void in
    self.placeLabel.text = weather.name
    self.updateUIWithWeather(weather: weather)
    self.iconImageView.image = nil
  }
}

Here you create a bunch of promises to fetch the weather for a selection of cities. These are then raced against each other with race(promises:). The then block will only be called when the first of those promises are fulfilled. In theory, this should be a random choice due to variation in server conditions, but it’s not a strong example. Also note that all of the promises will still resolve, so there are still five network calls, even though only one is cared about.

Build and run. Once the app is loaded, tap Random Weather.

Random Weather

Updating the condition icon and error handling is left as an exercise for the reader. ;]

Where to Go From Here?

You can download the fully finished sample project here.

You can read the documentation for PromiseKit at http://promisekit.org/, although it is hardly comprehensive. The FAQ http://promisekit.org/faq/ is useful for debugging information.

You may also want to read up on CocoaPods in order to install PromiseKit into your own apps and to keep up to date with their changes, as it is an active pod.

Finally, there are other Swift implementations of Promises. One popular alternative is BrightFutures.

If you have any comments, questions or suggestions for alternatives, promise to tell us below! :]

The post Getting Started with PromiseKit appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4399

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>