Making API calls with iOS Combine Future Publisher

Hemal Asanka
8 min readMay 5, 2023

--

What Is the Combine Framework?

The iOS Combine framework was introduced by Apple in 2019 as part of iOS 13. It is a declarative Swift framework for processing asynchronous events over time. This means that it allows you to write reactive programming code, which is a popular paradigm for handling asynchronous data streams.

Before the Combine framework, iOS developers had to rely on third-party reactive frameworks like RxSwift and ReactiveSwift to write reactive code. However, with the introduction of the Combine framework, iOS developers can now write reactive programming without depending on external libraries.

One of the advantage is that Combine Framework is built and supported by Apple, which means that it is a high-performance and powerful framework. Additionally, the Combine framework is designed to work seamlessly with other Apple frameworks and APIs, such as UIKit and SwiftUI.

If you’re already familiar with reactive programming, you’ll find that the Combine framework uses familiar concepts like publishers, subscribers, and operators to represent data streams and handle events. However, even if you’re new to reactive programming, the Combine framework provides a unified and easy-to-learn model for handling asynchronous data streams.

What Can We Do Using Combine?

Combine framework is provides a declarative model for handling asynchronous data streams. This means that we can use the framework’s APIs to handle closures, delegates, KVO, and notifications more efficiently, and transform, filter, and combine data streams in powerful ways.

For example, if we need to handle a chain of events, such as waiting for a network request to complete before updating the UI, we can use the Combine framework to write more concise and readable code. We can create a publisher that emits a value when the network request completes, and subscribe to that publisher to update the UI. This can make our code more streamlined and easier to reason about.

In the rest of this tutorial, we’ll explore the basics of the Combine framework and show you how to use it to write API calls.

How Does Combine Work?

Combine framework works on the basis of following three key concepts

1. Publisher

2. Subscriber

3. Operator

Combine is mainly working as publisher and subscriber model.

The Publisher is a protocol that defines a value type with two associated types: Output and Failure. Publishers allow the registration of subscribers and emit values to those subscribers. Publishers can be thought of as the source of the data stream.

The Subscriber is also a protocol, but it is a reference type. Subscribers can receive values from publishers and can be notified when the publisher has finished emitting values. The subscriber has two associated types: Input and Failure. Subscribers can be thought of as the destination of the data stream.

Here is the pattern for the Combine publisher and subscriber model.

The Pattern

Finally, Operators are extensions of publishers. Operators are methods that are called by the publisher and return a value to the publisher. Operators can be used to transform or manipulate the publisher’s associated types before passing them on to the subscribers. There may be multiple operators for a single publisher, allowing for complex data transformations and manipulations.

Depicting How to Attach Operators with Publishers and Subscribers

Making API Calls Using the Combine Framework

That was a quick introduction to the Combine framework. Now let’s dive into a real-world example of how to utilize it for making API calls.

In this example, we’ll be getting the top 50 iOS free apps used in the United States from the Apple RSS feed. You can generate your own RSS feed for future use at this website:
https://rss.applemarketingtools.com/

To accomplish this, we’ll call the following URL and load the result into a tableview:
https://rss.applemarketingtools.com/api/v2/us/apps/top-free/50/apps.json

Here is the JSON structure:

{
"feed": {
"title": "Top Free Apps",
"id": "https://rss.applemarketingtools.com/api/v2/us/apps/top-free/2/apps.json",
"author": {
"name": "Apple",
"url": "https://www.apple.com/"
},
"links": [
{
"self": "https://rss.applemarketingtools.com/api/v2/us/apps/top-free/2/apps.json"
}
],
"copyright": "Copyright © 2023 Apple Inc. All rights reserved.",
"country": "us",
"icon": "https://www.apple.com/favicon.ico",
"updated": "Mon, 1 May 2023 17:37:11 +0000",
"results": [
{
"artistName": "Temu",
"id": "1641486558",
"name": "Temu: Shop Like a Billionaire",
"releaseDate": "2022-08-31",
"kind": "apps",
"artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple116/v4/da/78/29/da7829de-6cdf-d3da-853f-24fb53186635/AppIcon-1x_U007emarketing-0-7-0-0-P3-85-220.png/100x100bb.png",
"genres": [],
"url": "https://apps.apple.com/us/app/temu-shop-like-a-billionaire/id1641486558"
},
{
"artistName": "Bytedance Pte. Ltd",
"id": "1500855883",
"name": "CapCut - Video Editor",
"releaseDate": "2020-04-14",
"kind": "apps",
"artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple126/v4/9d/17/31/9d1731e5-bab9-31c6-7cc9-ba2cb0d7690f/AppIcon-0-0-1x_U007emarketing-0-0-0-7-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/100x100bb.png",
"genres": [],
"url": "https://apps.apple.com/us/app/capcut-video-editor/id1500855883"
}
]
}
}

In this code, the results array is retrieved and the name and releaseDate for each app are displayed in the tableview.

Steps for Creating a Project:

  1. To decode the JSON data above, an API model struct will be created. The file will be named AppResponse.swift, and it will look like this:
import Foundation

struct AppsResponse: Codable {
let feed: Feed
}

// MARK: - Feed
struct Feed: Codable {
let results: [App]
}

// MARK: - Result
public struct App: Codable {
let artistName, id, name, releaseDate: String
let artworkUrl100: String
let url: String
}

2. Now, we are going to create a WebServiceManager.swift class to create a generic and common method for making API calls using the Combine framework.

import UIKit
import Combine // (1)

enum NetworkError: Error { // (2)
case invalidURL
case responseError
case unknown
}

extension NetworkError: LocalizedError {
var errorDescription: String? {
switch self {
case .invalidURL:
return NSLocalizedString("Invalid URL", comment: "")
case .responseError:
return NSLocalizedString("Unexpected status code", comment: "")
case .unknown:
return NSLocalizedString("Unknown error", comment: "")
}
}
}

class WebServiceManager: NSObject {
static let shared = WebServiceManager()

private var cancellables = Set<AnyCancellable>() // (3)

func getData<T: Decodable>(endpoint: String, id: Int? = nil, type: T.Type) -> Future<T, Error> {
return Future<T, Error> { [weak self] promise in // (4) -> Future Publisher
guard let self = self, let url = URL(string: endpoint) else {
return promise(.failure(NetworkError.invalidURL))
}
print("URL is \(url.absoluteString)")
URLSession.shared.dataTaskPublisher(for: url) // (5) -> Publisher
.tryMap { (data, response) -> Data in // (6) -> Operator
guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
throw NetworkError.responseError
}
return data
}
.decode(type: T.self, decoder: JSONDecoder()) // (7) -> Operator
.receive(on: RunLoop.main) // (8) -> Sheduler Operator
.sink(receiveCompletion: { (completion) in // (9) -> Subscriber
if case let .failure(error) = completion {
switch error {
case let decodingError as DecodingError:
promise(.failure(decodingError))
case let apiError as NetworkError:
promise(.failure(apiError))
default:
promise(.failure(NetworkError.unknown))
}
}
}, receiveValue: { data in // (10)
print(data)
promise(.success(data)
) })
.store(in: &self.cancellables) // (11)
}
}
}

Here are clarifications for the respective numbers mentioned above code.

(1). Importing Combine: import the Combine framework, which provides a declarative Swift API for processing values over time.

(2). Defining NetworkError: An enumeration is defined that conforms to the Error protocol. This enumeration is used to represent the various errors that may occur during the API call.

(3). Declare Set<AnyCancellable> object: A set of AnyCancellable objects is created to keep track of any Combine subscriptions that are made during the API call. These subscriptions will be canceled when the API call is completed or canceled.

(4). return Future: Future is a kind of Publisher. It is using asynchronous operation and returns Output and Error. It’s waiting until receive the data from API call.

(5). URLSession.shared.dataTaskPublisher(): Inside the future publisher we are writing another publisher subscriber model to get data from API. dataTaskPublisher() is a publisher for a URL session data task. It takes in a URL and returns a tuple of Data and URLResponse.

(6). tryMap(): This is a Combine operator that transforms the data emitted by the upstream publisher and may throw an error. In this case, it checks the HTTP status code of the response and throws an error if it is not in the 200–299 range: If there is not any error, it emit data.

(7). decode(): This is a Combine operator that decodes the upstream data using a JSONDecoder and returns a value of the specified generic type T. If the decoding fails, it will throw an error.

(8). receive(on: RunLoop.main): This is a Combine operator that specifies the scheduler on which to receive the value. In this case, it specifies the main thread’s RunLoop, which is necessary for updating the user interface.

(9). sink(receiveCompletion:receiveValue:): This is a Combine operator that creates a subscriber to receive the publisher’s values and errors.

(10). Handling receiveValue: This is the closure that inside the sink and it is executed when the Future successfully produces a value of type T. In this case, it simply prints the data to the console and fulfils the promise with the received value.

(11). store(in: &self.cancellables): This is a method of AnyCancellable that adds the subscription to the cancellables set. This allows the subscription to be canceled later if needed.

3. Let’s see how to call the above method to retrieve data for a UITableViewController subclass.

    private var apps:[App]? = nil
private var cancellables = Set<AnyCancellable>() // (1)

private func getApps() {
let spinner = UIActivityIndicatorView(style: .medium)
spinner.hidesWhenStopped = true
spinner.startAnimating()
tableView.backgroundView = spinner

let appURL = "https://rss.applemarketingtools.com/api/v2/us/apps/top-free/50/apps.json"

WebServiceManager.shared.getData(endpoint: appURL, type: AppResponse.self) // (2) -> Publisher
.sink { completion in // (3) -> Subscriber
spinner.stopAnimating()
switch completion {
case .failure(let err):
print("Error is \(err.localizedDescription)")
case .finished:
print("Finished")
}
} receiveValue: { [weak self] appResponse in
self?.apps = appResponse.feed.results
self?.tableView.reloadData()
} .store(in: &cancellables) // (4)
}

Here are clarifications for the respective numbers mentioned above code.

(1). Create a new empty set of cancellables to hold the subscriptions that you will make.

(2). Call the getData method of WebServiceManager, passing in the appURL and the AppsResponse.self type. This will return a Future publisher that will make an API request to retrieve data from the given endpoint.

(3). Attach two subscribers to the Future publisher using the sink method. One subscriber handles the completion event, while the other handles the data event.

(4). Store the subscriptions in the cancellables set to keep them in memory and allow them to be cancelled if needed.

That is pretty much the tutorial. You can download the source code using the following link.

Conclusion

In conclusion, the iOS Combine framework is a powerful tool for writing reactive programming in iOS applications. With its publisher-subscriber model and easy-to-use operators, the iOS Combine framework enables developers to easily manage and manipulate streams of data in a declarative and readable way. Although there may be a learning curve for new users, incorporating Combine into your codebase can lead to more efficient and maintainable code. Whether you’re working on small or large projects, using Combine can simplify your reactive programming. I hope this tutorial has given you a helpful introduction to Combine and how you can utilize it in your projects, and inspires you to explore more of its capabilities. If you have any questions or feedback, please leave them in the comments below.

Thank you for reading, and Happy coding!

--

--

Hemal Asanka

Lead Mobile Engineer and tech enthusiast. Passionate about building intuitive, user-friendly mobile applications.