Xcoding with Alfian

Software Development Videos & Tutorials

Fetching Remote Async API with Apple Combine Framework

Alt text

Combine is a framework that has just been recently released for all Apple platforms and it is included in Xcode 11. By using combine, it’s easier to process sequence of value over time whenever it is updated. It also helps us to simplify asynchronous code by not using delegation and avoiding complex nested callbacks.

There are several main components of Combine sych as:

  1. Publisher. It’s a protocol that provides interface to publish value to the subscribers. Sometimes, it’s also referred as the upstream source. It provides generic for the Input type and Failure error type. A publisher that never publish an error uses Never as the Failure type.
  2. Subscriber. It’s a protocol that provides for interface to subscribe value from publisher. It is the downstream destination in the sequence chain. It provides generic for Output type and Failure error type.
  3. Subject. It’s a protocol that provides interface to the client to both publisher and subscriber.
  4. Operator. By using operator, we can create a new publisher from a publisher by transform, filter, and even combine values from the previous or multiple upstream publishers.


Apple also provides several built in Combine functionality inside the Foundation framework such as Publishers for URLSession datatask, Notification, Timer, and KVO based property observing. Those built in interoperability really helps for us to integrate the framework into our current project.

To learn more about the basic of Combine such as, you can visit Ray Wenderlich site below. It’s a great introduction to learn about the basic of Combine. Ray Wenderlich intro to Combine link

What we will learn to build

In this article, we will learn on how we can leverage Combine Framework to fetch movies data using TMDb API. Here are the things that we will learn together:

  1. Sink operator to subscribe from Publisher using a closure.
  2. Future Publisher to produce a promise with single value or failure.
  3. URLSession Datatask Publisher to subscribe data published from an URL data task.
  4. tryMap operator to transform data into another Publisher .
  5. decode operator to transform data into a Decodable object and publish then to downstream.

The Starter Project

Before we start, you need to register for an API key in the TMDb website using the link at TMDb API Dev website.

You also need to download the starter project from the GitHub repository at Combine Starter project

Make sure to put the API key in the MovieStore class under the apiKey constant. Here are the the basic building blocks that the sample project provide us to build our project:

  1. Inside the Movie.swift file are the models that we will be use in our project. The root MoviesResponse struct implements Decodable protocol that we will use to decode JSON data into the model. It also contains results property that also conforms to decodable protocol.
  2. The MovieStoreAPIError enum that implements the Error protocol. Our API service will be using this enum to represent the error in the API such as URLError , DecodingError , and ResponseError.
  3. For the API Service, there is a MovieService protocol with one single method that fetch movies based on the endpoint passed. Endpoint itself is an enum that represent the endpoint in the TMDb API such as latest, popular, top rated movies.
  4. The MovieStore class is the concrete class that implements the MovieService protocol to fetch data using TMDb API. Inside this class, we will implement the fetchMovies method using Combine.
  5. The MovieListViewController class is the main View Controller that we will implement the subscription to fetch movies method that returns a Future , then update the TableView with the movies data using the new DiffableDataSourceSnapshot API.


Before we begin, let’s learn some of the basic Combine components that we will use to fetch the remote API.

Sink to subscribe from a Publisher using closure callback

The most simple way to subscribe from a Publisher is to use sink . With this, we can subscribe to a publisher using a closure whenever we receive new value or when the publisher finishes emitting the value.

let subscription = [1,2,3,4].publisher
.sink(receiveCompletion: { print($0)},
receiveValue: { print($0)})

Remember that in Combine , every subscription returns a Cancellable that will be disposed after the scope of the function finishes. To maintain the subscription for indefinite sequence, we need to store the subscription as a property.

Future as promise that publishes a single value or failure

Combine Future is pretty similar to the promise/future in other programming languages such as Javascript or Java. The Future can be used to provide Result asynchronously using a closure. The closure will be invoked with one parameter which is (Result<Output, Failure>) -> Void using Promise as typealias.

Future<Int, Never> { (promise)
  DispatchQueue.main.asyncAfter(deadline: .now() + 2)
    promise(.success(Int.random(in: 0...100))
  }
}

The snippet code above creates a Future with the success result of Int and failure error of Never. Inside the fulfil closure handler, we use DispatchQueue AsyncAfter to delay the execution after 2 seconds to mimic asynchronous behaviour. Inside the closure, we invoke the promise success passing random integer between 0 and 100.

Using Combine to Fetch Movies from TMDb API

To begin, open the starter project and navigate to fetchMovies method with empty implementation.

public class MovieStore: MovieService {  
  //...
  func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
    // TODO: Implement Future Publisher to fetch api
  }
}

Initializing and Returning Future of Movies

Let’s begin by initializing Future with callback handler, we will also return this Future. Inside the callback handler we invoke the generate

func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
  return Future<[Movie], MovieStoreAPIError> {[unowned self] promise in
    guard let url = self.generateURL(with: endpoint) else {
      return promise(.failure(.urlError(URLError(URLError.unsupportedURL))))
    }
    // TODO: Implement URL Session Data task publisher
  }
}

Using URLSession Datatask Publisher to fetch data from URL

To fetch and subscribe data from URL, we can utilize the built in URLSession datatask publisher method that accepts an URL as the parameter and returns a Publisher with Output of (data: Data, response: URLResponse) and Failure of URLError.

func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
  return Future<[Movie], MovieStoreAPIError> {[unowned self] promise
    //...
    self.urlSession.dataTaskPublisher(for: url)
    // TODO: Implement tryMap operator to chain publishers
  }
}

Transforming Publisher into another Publisher using tryMap Operator

Next, we will use the tryMap operator to create a new publisher based on the previous upstream publisher. Compared to map, tryMap can also throws an Error inside the closure that returns the new Publisher . In our case, we want to check the http response status code to make sure it is between 200 and 300. If not, we throws an responseError enum passing the status code. Otherwise, we just return the data as the downstream publisher.

func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
  return Future<[Movie], MovieStoreAPIError> {[unowned self] promise
    //...
    self.urlSession.dataTaskPublisher(for: url)
      .tryMap { (data, response) -> Data in
        guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
          throw MovieStoreAPIError.responseError((response as? HTTPURLResponse)?.statusCode ?? 500)
      }
      return data
    }
    // TODO: Implement decode of data into model
  }
}

Decoding Published JSON Data into MoviesResponse Publisher using Decode Operator

Next, we will utilize the decode operator that decodes the output JSON data from previous tryMap upstream publisher into the MovieResponse model using JSON Decoder.

func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
  return Future<[Movie], MovieStoreAPIError> {[unowned self] promise
    //...
    self.urlSession.dataTaskPublisher(for: url)
      .tryMap { (data, response) -> Data in
       // ...
    }
    .decode(type: MoviesResponse.self, decoder: self.jsonDecoder)
    // TODO: Implement Scheduler to make sure completion runs on main thread
  }
} 

Scheduling Received value from Publisher to run on Main Thread

To make sure the subscription handling runs on main thread, we can utilize the receive(on:) operator and pass the RunLoop.main to make sure the subscription receives the value in the main Run Loop.

func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
  return Future<[Movie], MovieStoreAPIError> {[unowned self] promise
    //...
    self.urlSession.dataTaskPublisher(for: url)
      .tryMap { (data, response) -> Data in
       // ...
    }
    .decode(type: MoviesResponse.self, decoder: self.jsonDecoder)
    .receive(on: RunLoop.main)
    .sink(receiveCompletion: { (completion) in
      if case let .failure(error) = completion 
        switch error {
          case let urlError as URLError:
            promise(.failure(.urlError(urlError)))
          case let decodingError as DecodingError:
            promise(.failure(.decodingError(decodingError)))
          case let apiError as MovieStoreAPIError:
            promise(.failure(apiError))
          default:
            promise(.failure(.genericError))
        }
      }
    }, receiveValue: { promise(.success($0.results)) })
  }
}

Subscribe with Sink using a closure callback to receive value and completion

Finally we arrived at the end of the chain, we’ll utilize sink to receive subscription from Publisher upstream. There are 2 things that you need to provide when initializing Sink, although one is optional:

  1. receiveValue:. This will be invoked whenever the subscription receives a new value from the publisher.
  2. receiveCompletion:(Optional). This will be invoked after the publisher finishes publishing the value, it passes the completion enum that we can use to check whether it finished with error or not.


Inside the receiveValue closure, we just passed invoke the promise passing the success case and the value which is the Movies array. In the receiveCompletion closure, we check if the completion has an error , then pass the appropriate error to the promise failure case.

func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
  return Future<[Movie], MovieStoreAPIError> {[unowned self] promise
    //...
    .receive(on: RunLoop.main)    
  }
}

Store the Subscription into Set<AnyCancellable> property

Remember that a subscription is an Cancellable , this type of protocol will be cancelled and cleared from the function scope, to make sure the subscription works until it finishes, we need to store the subscription into the instance property. In here, we use the Set<AnyCancellable> as the property and invoke store(in: passing the property to make sure the subscription still works after the function execution finished.

private var subscriptions = Set<AnyCancellable>()func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
  return Future<[Movie], MovieStoreAPIError> {[unowned self] promise
    //...
    .receive(on: RunLoop.main)
    .sink(receiveCompletion: { (completion) in
      // ...
    }, receiveValue: { promise(.success($0.results)) })
    .store(in: &self.subscriptions)
  }
}

That’s it, the fetchMovies method is completed and we are using combine operator to implement it!. Let’s try this API in our TableViewController.

Here is the complete implementation of the fetchMovies.

func fetchMovies(from endpoint: Endpoint) -> Future<[Movie], MovieStoreAPIError> {
      return Future<[Movie], MovieStoreAPIError> {[unowned self] promise in
          guard let url = self.generateURL(with: endpoint) else {
              return promise(.failure(.urlError(URLError(URLError.unsupportedURL))))
          }
          
          self.urlSession.dataTaskPublisher(for: url)
              .tryMap { (data, response) -> Data in
                  guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
                      throw MovieStoreAPIError.responseError((response as? HTTPURLResponse)?.statusCode ?? 500)
                  }
                  return data
          }
          .decode(type: MoviesResponse.self, decoder: self.jsonDecoder)
          .receive(on: RunLoop.main)
          .sink(receiveCompletion: { (completion) in
              if case let .failure(error) = completion {
                  switch error {
                  case let urlError as URLError:
                      promise(.failure(.urlError(urlError)))
                  case let decodingError as DecodingError:
                      promise(.failure(.decodingError(decodingError)))
                  case let apiError as MovieStoreAPIError:
                      promise(.failure(apiError))
                  default:
                      promise(.failure(.genericError))
                  }
              }
          }, receiveValue: { promise(.success($0.results)) })
              .store(in: &self.subscriptions)
      }
  }

Subscribe Movies from View Controller and Populate Table View with Movies Data

Navigate to the MovieListViewController.swift file and to the empty fetchMovies method. Here, we’ll invoke the movieAPI fetchMovies method passing the .nowPlaying endpoint as the parameter. In the returned Future, we subscribe using sink providing receiveCompletion closure handler that will check if there is an error and prompt an alert to the user displaying the error message. In receiveValue closure handler, we just invoke the generateSnapshot method passing the movies. This function will generate a new Diffable Data Source Snapshot using the movies and apply the snapshot to the TableView diffable datasource.

func fetchMovies() {
    self.movieAPI.fetchMovies(from: .nowPlaying)
        .sink(receiveCompletion: {[unowned self] (completion) in
            if case let .failure(error) = completion {
                self.handleError(apiError: error)
            }
        }, receiveValue: { [unowned self] in self.generateSnapshot(with: $0)
        })
        .store(in: &self.subscriptions)
}

Build and run the project, to see the combine publisher and subscriber in action 😋!. You can download the final project from the GitHub repository here. Combine Completed project

Conclusion

Using Combine framework to process sequence of value time asynchronously is really simple and easy to implement. The operators provided by the Combine are so powerful and flexible. We can avoid complex asynchronous code by using Combine by chaining upstream publishers, applying operators to downstream subscribers. SwiftUI also relies heavily on the Combine framework for it’s ObservableObject, Binding .

iOS developers have waited for a very long time for Apple to officially provide this kind of framework and finally they have provided the answer to us this year. For developers that has been using Rx for several years should not have the difficulty to adapt to the Combine framework as mostly the principle is the same between them. One last thing, let’s keep the lifelong learning goes on!.