Logo for tanaschita.com

Quick guide on Combine essentials in Swift

Learn basic Combine concepts and terms.

updated on 20 May 2024 · 10 min read

Apple's Combine framework provides a native way to write functional reactive code in our iOS applications. In this article, we'll go through basic Combine concepts and terms.

Let's directly jump in.

Sponsorship logo
Preparing for a technical iOS job interview - updated for iOS 17
Check out my book on preparing for a technical iOS job interview with over 200 questions & answers. Test your knowledge on iOS topics such as Swift, SwiftUI, Combine, HTTP Networking, Authentication, SwiftData & Core Data, Concurrency with async/await, Security, Automated Testing, Machine Learning and more.
LEARN MORE

Overview

The core components of Combine are publishers and subscribers. Publishers emit values or errors over time, while subscribers receive and handle these emissions.

In addition, Combine provides a rich set of operators that allow for transforming, filtering, and combining the emitted values.

Let's look at these main components in more detail with an example.

Publishers

Let's say, we are building an app that presents articles which can receive hearts. We want to be able to change the count of hearts for an article and also to notify subscribers about this change.

As a first step, we'll define a publisher which can send new values, in our case the new heart count.

For that, we'll use a subject which is a mutable publisher with the ability to send new values after its initialization:

struct Article {
// 3.
var heartsCountPublisher: AnyPublisher<Int, Never> {
heartsCountSubject.eraseToAnyPublisher()
}
// 1.
private let heartsCountSubject: CurrentValueSubject<Int, Never>
// 2.
init(heartsCount: Int) {
heartsCountSubject = CurrentValueSubject(heartsCount)
}
// 4.
func addHeart() {
heartsCountSubject.send(heartsCountSubject.value + 1)
}
}

Let's break down the code step by step:

  1. We define a subject named heartsCountSubject as a private variable of type CurrentValueSubject<Int, Never>. We will use it to send new values to subscribers. Since a subject is a generic type we specify its Output and Failure type. The output type defines what kind of values the subject will send, in our case Int values. Since our example doesn't involve errors, we use Never as error type.

Note: there are two subject types available:

  • CurrentValueSubject: This subject type maintains access to the current value.
  • PassthroughSubject: This subject type simply passes values through and does not maintain access to the current value.
  1. We define an initializer that takes a heartsCount parameter to set the initial value for heartsCountSubject.

  2. We define a publisher named heartsCountPublisher of type AnyPublisher<Int, Never>. It has the same Output and Failure type as our subject. Although we could make our subject public, using a publisher allows us to expose only the subscription functionality to the outside world, preventing external entities from sending new values.

  3. We define an addHeart method for increasing the count of hearts. Here, we use our subject to send a new value. To be able to add a heart we access the subject's value property to get the current value. That's the reason why we used a CurrentValueSubject and not a PassthroughSubject - only a CurrentValueSubject has access to its current value.

Subscribers

A publisher can have one or more subscribers. These subscribers request values and receive them when they are published.

In our example, we can use the Article struct and subscribe to its hearts count as follows:

// 1.
let article = Article(heartsCount: 5)
// 2.
let heartsCountSubscriber = article.heartsCountPublisher
.sink { value in
print(value)
}
// 3.
article.addHeart()
article.addHeart()

Let's go through this code step by step.

  1. We create an Article instance with an initial hearts count of 5.

  2. We create a subscriber named heartsCountSubscriber that listens for updates on the hearts count. The subscriber uses the publisher's sink(receiveValue:) method to subscribe for updates. Now, every time the hearts count changes, the closure is called, printing the new value.

  3. We increase the count of hearts by calling addHeart() twice.

Therefore, the code produces the following output:

5
6
7

When we create a new subscriber, the publisher returns an object that conforms to the Cancellable protocol. This allows us to cancel receiving new values by calling the cancel() method on the subscriber.

For example:

article.addHeart()
heartsCountSubscriber.cancel()
article.addHeart()

In the code above, the last increment is not printed because the subscriber has been canceled.

Operators

Operators modify values sent from publishers, allowing us to transform, filter, and combine data streams.

In our example, we can use operators to modify received values before executing the print closure. For instance, to print a more descriptive message, we can use the map operator to convert Int values to String values:

let heartsCountSubscriber = article.heartsCountPublisher
.map { "\($0) hearts" }
.sink { print($0) }

Now, our output looks as follows:

5 hearts
6 hearts
7 hearts

We can chain multiple operators to build complex data processing pipelines. Let's explore a more detailed example:

let publisher1 = PassthroughSubject<[Int], Never>()
let publisher2 = PassthroughSubject<[Int], Never>()
publisher1
.merge(with: publisher2) // 1.
.flatMap { Publishers.Sequence(sequence: $0) } // 2.
.filter { $0 % 2 == 0 } // 3.
.dropFirst() // 4.
.collect() // 5.
.sink { print($0) }
publisher1.send([1, 3])
publisher2.send([6, 10])
publisher2.send([4, 19, 8])
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)

While this example may not perform a useful task, it demonstrates how operators work and how they can be combined. Try to predict the output of each operator before checking the explanation below.

  1. merge: Combines elements from publisher1 and publisher2. Output: [1, 3] [6, 10] [4, 19, 8].
  2. flatMap: Flattens the array into a sequence of Int values. Output: 1, 3, 6, 10, 4, 19, 8.
  3. filter: Keeps only even values in our example. Output: 6, 10, 4, 8.
  4. dropFirst: Drops the first element. Output: 10, 4, 8.
  5. collect: Collects all received items into an array. Output: [10, 4, 8]

To explore more operators, visit the official Publisher documentation.

Memory management in Combine

In Combine, memory management is crucial to prevent memory leaks and retain cycles. The lifecycle of a subscriber is linked to the lifecycle of the retaining object. When this object is deallocated, the cancel method is automatically called on the subscriber, releasing it as well.

To avoid creating retain cycles, especially when using self within a sink closure, we need to manage memory carefully. One common approach is to use the AnyCancellable type. When we subscribe to a publisher, it returns an AnyCancellable instance which we can store and manage appropriately. For example:

var cancellables = Set<AnyCancellable>()
publisher.sink {
// ...
}
.store(in: &cancellables)

In this example, we:

  1. Use [weak self] to avoid a retain cycle, ensuring that self is not strongly captured by the closure.
  2. Store the AnyCancellable instance returned by sink in a Set called cancellables.

When the object holding the cancellables set is deallocated, the AnyCancellable instances in the set are automatically cancelled. These steps prevent memory leaks and ensure that subscriptions are properly terminated.

Sponsorship logo
Preparing for a technical iOS job interview - updated for iOS 17
Check out my book on preparing for a technical iOS job interview with over 200 questions & answers. Test your knowledge on iOS topics such as Swift, SwiftUI, Combine, HTTP Networking, Authentication, SwiftData & Core Data, Concurrency with async/await, Security, Automated Testing, Machine Learning and more.
LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

combine

reactive programming

swift

Cheat sheet on Combine operators for iOS development

Get an overview on Publisher operators in Combine.

21 Nov 2022 · 5 min read

Latest articles and tips

© 2024 tanaschita.com

Privacy policy

Impressum