An Intro to Apple’s Combine Framework
By: Tribalscale Inc.
Tags: Swift, Development, How To, Apple, Framework

by Daniel Carmo, Agile Software Engineer

Photo by AltumCode

Combine was announced by Apple in the 2019 WWDC. This new framework is used to implement Reactive Programming natively in Swift.

The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events.

Combine uses three core concepts in order to create easier to read and maintainable code:

Publishers

Subscribers

Operators

Here’s a quick breakdown of the core concepts:

Publishers

Publishers are items that describe what values and errors can be produced. Publishers are value types to be defined as structs and allow for registration by Subscribers, which receive the values they produce. The Publisher protocol is defined as follows:

(WWDC 2019 — Introducing Combine)

The Publisher includes an associatedtype for the Output and Failure. The Output describes the type of value that the Publisher produces. The Failure describes the type of errors that it produces. In the event that no error can be emitted a special type of Never can be used. As you can see from the protocol, the Publisher has one function which is to have a Subscriber subscribe to it’s produced values. Based on the constraints of the function, the Subscriber must accept an Input of the Output type and a Failure type matching the Publisher.

Subscribers

Subscribers are items that describe the consumption of values from Publishers. These values attach to publishers, consuming values until the completion is reached. Subscribers are used to act on the Published values and hence are reference types and are declared as classes in Swift. The Subscriber protocol is defined as follows:

(WWDC 2019 — Introducing Combine)

The Subscriber includes an associatetype for Input and Failure. The Input describes the type of value that they expect to receive. The Failure describes the type of errors that they expect to receive. If no error is expected then they can use the Never type. The Subscriber has more methods on it than the Producer. Each method is invoked at different times in the lifecycle of the subscriber. When a subscriber initiates the subscribe process on a Publisher, the Publisher responds to it by creating a Subscription and sending it via the receive(subscription: Subscription) method. From here the Subscriber then demands N objects from the Publisher and begins receiving them on the receive(_ input: Input) -> Subscribers.Demand function. The Publisher will either run out of values to send or reach the demanded number of values and return on the receive(completion: Subscribers.Completion<Failure>) method. As demonstrated by the figure below:

(WWDC 2019 — Introducing Combine)

Operators

Operators are the last piece to the Combine Framework. Operators are used to transform values from a Publisher to a more suitable value to be consumed by a Subscriber. They subscribe to the upstream (Publisher) and produce new values for the downstream (Subscriber). The operator is a small, self contained piece of functionality that can be chained together to produce a more complex final result.

Let’s take a look at the Map Operator. The Map operator is described as follows:

(WWDC 2019 — Introducing Combine)

The Map Operator has the generic type Upstream which must extend a Publisher and an Output type which the values are transformed from. You will also notice that the Map struct extends from Publisher. The constructor of Map expects an upstream, and a transformation function that takes the Upstream.Output and converts it into it’s expected Output type. As you can see this is an extension on Publishers, so in order to use this method we’d have to use the constructor to accept the Upstream and transform the values. Luckily Apple provides us with a helper method which extends directly on a Publisher.

(WWDC 2019 — Introducing Combine)

This allows us to use an existing Publisher and apply this Operator directly to it.

Example 1

Let’s take a look at an example of the above in action.

[“12”, “15”, “20”, “10”]

// Convert the array of Strings into a Publisher

.publisher

// Convert array of Strings to integers

.map { Int($0) ?? 0 }

// Double the values

.map { $0 * 2 }

// Subscribe to the Publisher with a sink and print the values

// Up until this point, the Publisher has emitted no values and done no work. Once we subscribe it begins publishing it’s values

.sink(receiveCompletion: { _ in

print(“COMPLETE”)

}, receiveValue: {

print(“Doubled Value \($0)”)

})

In this simple example, we are taking an Array of String and converting it to a Publisher. We pass the values through the map Operator and convert them into Int values. After which we multiply the values by 2 through another map Operator and then we use the sink Subscriber to begin receiving and printing the values. In the end we have the following printed:

Doubled Value 24

Doubled Value 30

Doubled Value 40

Doubled Value 20

COMPLETE

You can see that the Publisher has produced four values from the array to the Subscriber sink and once exhausted called complete on the Subscriber.

Let’s take a look at converting the above into a function:

func convertStringArrayToIntArrayAndDoubleValues(_ values: [String]) -> AnyPublisher<Int, Never> {

return values

// Convert array into a publisher

.publisher

// Convert array of Strings to integers

.map { Int($0) ?? 0 }

// Double the values

.map { $0 * 2 }

// Erase the resulting type to AnyPublisher

.eraseToAnyPublisher()

}

There’s a few key things that are happening here.

First let’s take a look at the function definition. Initially we are taking in an Array of String and then returning a type of AnyPublisher<Int, Never>. This AnyPublisher is a special type that helps us clean up the intermittent Publishers that are created by using Operators.

Notice that the AnyPublisher return uses an Int as it’s output type. The Publisher will emit a single integer at a time as it’s converted instead of emitting the entire String converted to a single array. This means the Subscriber can act on the individual Ints as they are converted.

Lastly take a look at the last line of the function. This line erases the intermittent types of the Publishers that are created when we chain the Operators together. Without the eraseToAnyPublisher() our return statement would be a large string of Publishers chained together. This method helps us perform some type erasure to the final type that is expected of the method.

Example 2

Apple has already converted some of the functionality of Foundation to use Publishers. Currently there is support for Timer, NotificationCenter, and URLSession. Let’s take a look at URLSession for a more practical example. For this example we’ll use the Cat Facts API to get some facts about animals (https://alexwohlbruck.github.io/cat-facts/docs).

First we’ll create the model for parsing:

struct CatFact: Decodable {

let _id: String

let test: String

let type: String

}

For this example we’ll just concern ourselves with the _id, text, and type of fact. Next let’s create our URLRequest and send the request with URLSession.

var request = URLRequest(url: URL(string: “https://cat-fact.herokuapp.com/facts/random")!)

request.httpMethod = “GET” let cancellable = URLSession.shared.dataTaskPublisher(for: request)

.tryMap { element -> Data in

guard let httpResponse = element.response as? HTTPURLResponse,

httpResponse.statusCode == 200 else {

throw URLError(.badServerResponse)

}

return element.data

}

.decode(type: CatFact.self, decoder: JSONDecoder())

.sink(receiveCompletion:{ completion in

print(“COMPLETED PROCESSING \(completion)”)

}, receiveValue: { (catFact) in

print(“We received a \(catFact.type) Fact!”)

print(“\(catFact.text)”)

})

In this example we are using the URLSession’s new dataTaskPublisher method. This method returns us a Publisher that we can then Operate and Subscribe to. We are using tryMap here to map the response to its data and then follow up with decode into our CatFact type. Lastly we are Subscribing with sink and printing out the value of the Cat Fact and then receive a completion. You can see with this there’s no longer a callback, but instead clear concise steps that we take in the form of Operators to get to our final step, the fun cat fact that we requested!

We received a cat Fact!

The Havana Brown breed hails from England, where it was created by crossbreeding Siamese cats with domestic black cats.

COMPLETED PROCESSING finished

Conclusion

Swift has become even more powerful with the addition of the Combine Framework. It gives us native Reactive Programming functionality that allows us to use the MVVM pattern more closely. This is a basic introduction to the layout and flow of Publishers, Operators, and Subscribers. There is a lot more to cover than the above and I’m very excited to unlock the full potential of Combine as it becomes more widely used. Combine and SwiftUI work together seamlessly and unlock greater potential for the MVVM pattern. The one major factor holding back using this fully in production is that it requires a minimum version of iOS 13. As applications continue to increase their minimum target versions, Combine will become the driving force behind our applications!

Resources

Introduction to Combine

https://developer.apple.com/videos/play/wwdc2019/722/

Combine in Practice

https://developer.apple.com/videos/play/wwdc2019/721

Processing URL Session Data Task Results with Combine

https://developer.apple.com/documentation/foundation/urlsession/processing_url_session_data_task_results_with_combine

Cat Facts API

https://alexwohlbruck.github.io/cat-facts/docs