Combine for Dummies: Part 1 — Making Asynchronous Swift Easy

Edoardo Troianiello
4 min readAug 1, 2023

--

Combine is Apple’s way of offering a reactive programming framework for Swift. Imagine you’re a chef in a busy kitchen, and every time an order comes in, you have to manage the sequence and timing perfectly. In programming, Combine helps manage such sequences, especially with asynchronous events like user input, network requests, and more.

Let’s start with some basics.

Basics of Combine

  • Publisher: Think of a Publisher as a radio station. It broadcasts songs (or in our case, data). In Combine, a Publisher is an object that emits values over time. For example, a network call that returns data can be seen as a publisher broadcasting the fetched data.
  • Subscriber: If the Publisher is a radio station, then the Subscriber is the radio at your home. It tunes in and listens to whatever the Publisher is broadcasting. The subscriber receives and processes the data emitted by the Publisher.
  • Subscription: This is the magic that connects the radio station (Publisher) to your radio (Subscriber). Once connected, the music (or data) flows.

Anatomy of a Publisher

At the heart of Combine is the concept of a Publisher. Think of a Publisher as a broadcaster: it emits a series of values over time. When a Publisher emits a value, it travels through the pipeline, potentially getting transformed by operators, until it finally reaches the Subscriber. This journey is known as the life cycle of an event in Combine.

Types of Publishers:

Combine offers various built-in publishers:

  • Just: Emits a single value and then completes.
  • Future: Represents a single value that will be provided later.
  • Sequence: Turns a regular sequence (like an array) into a publisher.
  • Empty: A publisher that never sends any values and immediately completes.
let justPublisher = Just("Hello, Combine!")
let futurePublisher = Future<String, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
promise(.success("Future's value"))
}
}
let sequencePublisher = ["A", "B", "C"].publisher

Output & Failure Types:

Every Publisher declares what type of data it will emit (Output) and what type of errors it might produce (Failure). For example, a publisher emitting integers and never failing would have types Output = Int and Failure = Never.

The Subscriber’s Role

For a Publisher to start broadcasting, you need a Subscriber. A Subscriber listens to the values (and possibly errors) a Publisher emits.

  • Subscribing Using Sink: The simplest way to subscribe to a publisher is using the sink method:
let numbers = [1, 2, 3, 4, 5].publisher

numbers.sink(receiveValue: { value in
print(value)
})

In Combine, once you’ve set up a subscription, it’s crucial to remember to break the connection when it’s no longer needed to free up memory. This connection is handled by something called Cancellable. Think of it as an agreement. Once the agreement is over, you cancel it.

Cancellables: Every subscription returns a “cancellable.” This is a token representing the active subscription. You need to keep this around for as long as you want the subscription to remain active. When you discard the cancellable, the subscription is cancelled.

let cancellable = numbers.sink { print($0) }
// To cancel the subscription later:
cancellable.cancel()

Transformation & Modification

Once you have a flow of data from a Publisher to a Subscriber, you can introduce operators. These are methods provided by Combine to modify or transform the data.

  • map: This lets you change data into another form. For example, if your data is a number and you want to double it, you’d use map.
  • filter: This lets only certain pieces of data through. Like picking only the ripe apples from a basket.
  • merge: If you have data coming from two places and you want to handle them as one, you use merge.
let numbers = [1, 2, 3, 4].publisher
numbers.map { $0 * 2 }.sink { print($0) }
// Prints 2, 4, 6, 8
numbers.filter { $0 % 2 == 0 }.sink { print($0) }
// Prints 2, 4
let numbers1 = [1, 2].publisher
let numbers2 = [3, 4].publisher
Publishers.Merge(numbers1, numbers2).sink { print($0) }
// Prints 1, 2, 3, 4

Practical Examples: Networking with Combine

Networking in iOS traditionally relied on completion handlers and delegate methods. With Combine, you can craft more readable and maintainable network calls. The result is a reactive approach to handling network responses.

Using URLSession with Combine

URLSession has a dataTaskPublisher(for:) method, which returns a publisher.

let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!

let cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [Post].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print("Error: \(error)")
}
}, receiveValue: { posts in
print("Received posts: \(posts)")
})

Chaining Multiple Requests

Suppose you first want to fetch a user and then fetch all posts written by that user:


let userURL = URL(string: "https://jsonplaceholder.typicode.com/users/1")!

let cancellable = URLSession.shared.dataTaskPublisher(for: userURL)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
.flatMap { user -> URLSession.DataTaskPublisher in
let postsURL = URL(string: "https://jsonplaceholder.typicode.com/posts?userId=\(user.id)")!
return URLSession.shared.dataTaskPublisher(for: postsURL)
}
.map(\.data)
.decode(type: [Post].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in }, receiveValue: { posts in
print("Received posts: \(posts)")
})

Conclusion

From the foundational concepts of publishers and subscribers to the practical applications in networking, we’ve seen how Combine can redefine the way we approach asynchronous tasks in Swift. Its flexibility and power are undeniable. But remember, we’ve only scratched the surface. As we transition into the next phase, keep these principles in mind. The deeper dive in our upcoming sections will further illustrate the vast potential of Combine. Ready for more? Let’s dive into part two!

--

--

Edoardo Troianiello
Edoardo Troianiello

Written by Edoardo Troianiello

Computer Engineer | iOS Developer | Alumni @Apple Developer Academy in Naples