Waldo sessions now support scripting! – Learn more
App Development

How to Implement PromiseKit in Swift Apps

Nabendu Biswas
Nabendu Biswas
How to Implement PromiseKit in Swift Apps
January 17, 2023
12
min read

Promises are one of the best ways to handle asynchronous code. The best example of asynchronous code is API making a call to some endpoint and getting the data.

Now, these calls are the basis of the modern internet. This is how Amazon's website receives all product data from the back end. The data is always stored in a server, so it takes one to two seconds to show the data.

That's a lot of time in programming, and our code doesn't wait before executing further. Once it receives the data, it's displayed.

In this post, we'll learn about PromiseKit, through which we can implement promises in Swift.

We'll create a small app and see an example without promise first. After that, we'll implement promises through PromiseKit.

Additionally, we'll see a complex example of multiple promises. Finally, we're going to write a small test for our app.

PromiseKit is an open-source package through which we implement promises in Swift

What Is a PromiseKit Function?

PromiseKit is an open-source package through which we implement promises in Swift. We can also write asynchronous code without promises, and that's by using callbacks, which we've done in our Alamofire post.

The problem with callbacks is that the code is complex. And if we have to do several asynchronous calls, the code becomes more complex. This is sometimes also referred to as Callback Hell.

In the next section, we're going to look into doing an asynchronous call with callbacks and follow it by doing an asynchronous call with promises.

Project Setup

Let’s first create a new project by opening XCode. After that click on New > Project.

new project xcode

In the next pop-up, click on App and then the Next button.

choose template for new project

In the next pop-up, give any product name, which is PromiseKitDemo in our case. The interface should be Storyboard, and we also need to select the checkbox to Include Tests because we’re going to use both Storyboard and tests in the app.

name the project as promisekitdemo

In the next pop-up, click on the Create button after choosing a suitable folder.

In the next pop-up, click on the Create button after choosing a suitable folder.

Our project will open in XCode and will look like this:

Our project will open in XCode and will look like this:

Installing Dependencies

We're going to use both PromiseKit and Alamofire in this project. The question is "How do I use PromiseKit?" and the answer is simple—we can use any external package in Swift by installing it through Podfile or the Swift Package Manager.

In this post, we're going to use the Swift Package Manager to install both.

Now, from the Alamofire GitHub page, get the Git link by clicking on "Code."

Next, go to the root of the project. After that, click on the project and go to the Package Dependencies tab.

Then, click on the + icon.

Then, click on the + icon.

A pop-up will be opened to add packages. Here, we'll see a lot of Swift packages.

A pop-up will be opened to add packages.

In the search bar give the Git link for Alamofire, which we copied earlier. We'll get the package, and then click on the Add Package button.

In the search bar give the Git link for Alamofire, which we copied earlier. We'll get the package, and then click on the Add Package button.

A pop-up will appear to confirm we want to add the package. Here, we'll click on the Add Package button.

A pop-up will appear to confirm we want to add the package. Here, we'll click on the Add Package button.

Next, we'll add PromiseKit to our project. So, go to the PromiseKit GitHub link and get the Git URL by clicking on "Code."

Next, we'll add PromiseKit to our project. So, go to the PromiseKit GitHub link and get the Git URL by clicking on "Code."

Again, click on the + icon in the Swift Package Manager. Then, in the pop-up, give the PromiseKit Git link. Upon getting the package, click on the Add Package button.

Again, click on the + icon in the Swift Package Manager.

Again, a pop-up will appear to confirm we want to add the package. Here, also click on the Add Package button.

Again, a pop-up will appear to confirm we want to add the package. Here, also click on the Add Package button.

Now, in XCode, we'll see both packages have been installed.

Now, in XCode, we'll see both packages have been installed.

API Call Through Callbacks

Now, we'll first do the API call using the earlier used callback method.

Create a new Swift file in the root folder from XCode.

Create a new Swift file in the root folder from XCode.

In the next pop-up, give the new file a name: APIFetchHandler.

In the next pop-up, give the new file a name: APIFetchHandler.

Now, in the file APIFetchHandler.swift, first import Alamofire. After that, inside the class APIFetchHandler, we'll create its static instance.

Next, we have the function fetchAPIData(), in which we're using the AF.request() method to do an API call to the posts JSON placeholder endpoint.

In case of the success of the API request, we'll take the data and use JSONDecoder() to convert it into JSON. In the case of failure, we'll just print it.

Also, notice that we're using a model called Codable, which has a structure that needs to be the same as the JSON placeholder endpoint object.

    
      import Foundation
      import Alamofire
      
      class APIFetchHandler {
          static let sharedInstance = APIFetchHandler()
         func fetchAPIData() {
            let url = "https://jsonplaceholder.typicode.com/posts";
            AF.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil)
              .response{ resp in
                  switch resp.result{
                    case .success(let data):
                      do{
                        let jsonData = try JSONDecoder().decode([Model].self, from: data!)
                        print(jsonData)
                     } catch {
                        print(error.localizedDescription)
                     }
                   case .failure(let error):
                     print(error.localizedDescription)
                   }
              }
         }
      }
      
      struct Model:Codable {
         let userId: Int
         let id: Int
         let title: String
         let body: String
      }
    

Now, in the ViewController.swift file, call the fetchAPIData() function:

    
      APIFetchHandler.sharedInstance.fetchAPIData()
    
Now, in the ViewController.swift file, call the fetchAPIData() function:

Then, we'll follow the instructions in the section App Layout with Storyboard from the Alamofire post.

Back in the APIFetchHandler.swift file, pass the parameter of a handler. Now, instead of printing jsonData, we're calling it with the function handler. This is the concept of the callback function to use the API data.

Back in the APIFetchHandler.swift file, pass the parameter of a handler. Now, instead of printing jsonData, we're calling it with the function handler.

Now, in the ViewController.swift file, we've changed the call to fetchAPIData. Here, we're storing the data in the apiResult variable. The data we're getting back here is because of the callback function passed in parameters to the fetchAPIData function in the APIFetchHandler.swift file.

We've also created an extension of ViewController from the Delegate and DataSource. It'll be used to show the data on the simulator.

    
      class ViewController: UIViewController {
        var apiResult = [Model]()
        @IBOutlet var apiDataView: UITableView!
        override func viewDidLoad() {
             super.viewDidLoad()
             APIFetchHandler.sharedInstance.fetchAPIData{ apiData in
               self.apiResult = apiData
    
               DispatchQueue.main.async {
                 self.apiDataView.reloadData()
              }
           }
       }
    }
    
    extension ViewController: UITableViewDelegate, UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
           return apiResult.count
        }
    
       func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
          guard let cell = tableView.dequeueReusableCell(withIdentifier: "cellProto")
          else {
            return UITableViewCell()
         }
         cell.textLabel?.text = apiResult[indexPath.row].title
         return cell
      }
    }
    


Now, upon running the app, we'll see the titles from the JSON placeholder API endpoint on display.

Now, upon running the app, we'll see the titles from the JSON placeholder API endpoint on display.

API Call Through Promises

Back in the APIFetchHandler.swift file, we'll first import PromiseKit. Then, we'll create a new function called fetchDataWithPromise(), where we're also returning a promise.

Since a promise has three states—pending, fulfill, and reject—we'll first create a resolver and assign it to the pending state.

Next, we'll do the API call with Alamofire, as described earlier. But once we get the response, we have a code for the error. The code will be thrown if we have an error that involves resolver.reject(error).

If the API call is successful and we get the data, we'll send it back with resolver.fulfill(posts).

    
      import Foundation
      import Alamofire
      import PromiseKit
      
      class APIFetchHandler {
      ...
        func fetchDataWithPromise() -> Promise<Any> {
          let (promise, resolver) = Promise<Any>.pending()
          AF.request(postUrl, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil)
             .response{ resp in
               if let error = resp.error{
                    resolver.reject(error)
               }
      
               if let data = resp.data {
                 do{
                   let posts = try JSONDecoder().decode([Model].self, from: data)
                   resolver.fulfill(posts)
                 }catch{
                   resolver.reject(error.localizedDescription as! Error)
                 }
                 }else{
                   resolver.reject("Posts not found" as! Error)
                }
            }
            return promise
         }
      }
    
If the API call is successful and we get the data, we'll send it back with resolver.fulfill(posts).

Now, in ViewController.swift file, we'll call the function fetchDataWithPromise(). Notice that we have the done and catch block. The done block is run if fulfill is executed from the fetchDataWithPromise(). And the catch block is run if reject is executed from the fetchDataWithPromise().

In our case, since we had success in the API call and the fulfill was executed, we'll go to the posts printed on the console.

    
      APIFetchHandler.sharedInstance.fetchDataWithPromise()
      .done{ posts -> Void in 
          print("Promise posts: \(posts)")
      } .catch { error in 
          print("Something went wrong: \(error)")
      }
    
In our case, since we had success in the API call and the fulfill was executed, we'll go to the posts printed on the console.

Now, we'll also check the case in which we get the error. But first, we need to have an enum containing errors in the APIFetchHandler.swift file.

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

Also, we've updated the error to take from the enum in resolver.reject() in the APIFetchHandler.swift file. The main thing here is that we've made the API URL endpoint wrong.

Also, we've updated the error to take from the enum in resolver.reject() in the APIFetchHandler.swift file.

Now, when we run our app again, an error will be printed. This time, reject is executed from the fetchDataWithPromise(). And the catch statement is run in the ViewController.swift file.

Promises in Parallel

Back in our APIFetchHandler.swift file, we'll add a function called fetchUsersWithPromise(). This is the same as our earlier function of fetchDataWithPromise() but calls the different JSON placeholder endpoint of users.

Note that we've also made a new model called "User" for it.

    
      func fetchUsersWithPromise() -> Promise<Any> {
        let (promise, resolver) = Promise<Any>.pending()
        let url = "https://jsonplaceholder.typicode.com/users";
        AF.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil)
          .response{ resp in
            if let error = resp.error{
              resolver.reject(error)
            }
      
            if let data = resp.data {
              do{
                let users = try JSONDecoder().decode([User].self, from: data)
                resolver.fulfill(users)
             }catch{
                resolver.reject(ApplicationError.usersCouldNotBeParsed)
            }
           }else{
              resolver.reject(ApplicationError.noUsers)
          }
        }
        return promise
      }
      
      struct User: Codable{
        let id: Int
        let name: String
        let username: String
        let email: String
      }
    

Now, in the ViewController.swift file, we'll first have two variables: getAllUsers and getAllPosts. They'll have the data from the call to the functions fetchUsersWithPromise() and fetchDataWithPromise(), respectively.

Next, we have a "when" statement, which will take both these variables. It'll execute them in parallel and wait for them to finish.

Inside it, in the done statement, we'll print both of them. We also have a catch statement in case of failure.

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

Upon running the app again, we'll get data from both API calls.

Upon running the app again, we'll get data from both API calls.

In case of an error in promises in parallel, we won't get any result, and the error will be shown. Again, we have a wrong URL for posts in the APIFetchHandler.swift file.

Upon running the app, we'll get the error shown below.

Upon running the app, we'll get the error shown below.

Testing the App

We'll do a simple XCTest in our app for the post API URL. So, we've moved the post API URL outside and made it public in the ViewController.swift file.

We've also removed it from the fetchAPIData() and fetchDataWithPromise() functions while using the new reference of postUrl in both methods.

We've also removed it from the fetchAPIData() and fetchDataWithPromise() functions while using the new reference of postUrl in both methods.

First, go to the PromiseKitDemoTests.swift file inside the PromiseKitDemoTests folder. Here, we'll create an instance of APIFetchHandler. After that, use the XCTAssertEqual function and check whether the URL is equal to https://jsonplaceholder.typicode.com/posts.

    
      let apiFetchData = APIFetchHandler()
      XCTAssertEqual(apiFetchData.postUrl, "https://jsonplaceholder.typicode.com/posts")
    

Now, run this test by clicking on the play button beside the testExample(). Our test passed, and we're also getting confirmation in the console.

Now, run this test by clicking on the play button beside the testExample(). Our test passed, and we're also getting confirmation in the console.

What We’ve Learned

In this post, we’ve talked about promises for doing asynchronous API calls. We can do the same in Swift using the open-source package of PromiseKit.

We created a simple project in XCode, then hit the JSON placeholder API endpoint.

First, we saw an example with a callback. After that, we converted that to use promises.

Next, we saw a complex example with parallel promises. We also wrote simple test cases using XCTest to test just a string.

But to test the API calls is a difficult task, as it requires complex mocking logic. Instead of that, we can use Waldo to perform such tests. You’re only required to provide the APK or IPA file. Waldo automatically generates test cases and emails you a nice report.

Check out this link to learn more about SwiftUI.

Automated E2E tests for your mobile app

Waldo provides the best-in-class runtime for all your mobile testing needs.
Get true E2E testing in minutes, not months.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.