TANASCHITA.COM
Articles about Swift and iOS Development by Natascha Fadeeva

Quick guide on Combine essentials

Get started with Combine by reading this guide on basic Combine concepts and terms.

Image of the author.
Published 15. November 2020 - 10 min read

It was really exciting to hear about Apples new framework called Combine at WWDC 2019. Finally, we have a native way to write functional reactive code and to build apps in a declarative way.

Overview

The main components of Combine are Publisher, Subject, Subscriber and Operator. Here is a brief summary of what they do:

  • Publisher
    • exposes values of a certain type over time
    • can be completed or optionally fail with an error
  • Subject
    • a mutable publisher
    • has the ability to send new values after it's initialization
    • there are two subject types available:
      • CurrentValueSubject - as the name indicates, this subject type has access to the current value
      • PassthroughSubject - as the name indicates, this subject passes the current value through, i.e. it has no access to it
  • Subscriber
    • receives values from publishers / subjects
  • Operator
    • modifies values that are send from publishers / subjects

In the following figure, we can see these components in action. We will go through the example step by step and in more detail below.

Combine's main components cheat sheet

Imagine, we have an app that presents articles to the user. The articles can receive likes. As a requirement, we want to be able to change the count of likes for every article and also to notify subscribers about this change.

Publishers & Subjects

As the first step, we use a combination of Combine's publisher and subject to achieve our goal.

struct Article {

    // 3.
    var likesCountPublisher: AnyPublisher<Int, Never> {
        likesCountSubject.eraseToAnyPublisher()
    }

    // 1.
    private let likesCountSubject: CurrentValueSubject<Int, Never>

    // 2.
    init(likesCount: Int) {
        likesCountSubject = CurrentValueSubject(likesCount)
    }

    // 4.
    func addLike() {
        likesCountSubject.send(likesCountSubject.value + 1)
    }
}

Let's go through this code step by step.

  1. We define a subject named likesCountSubject as a private variable of type CurrentValueSubject<Int, Never>. We will use this subject to send new values to subscribers. Since the subject is a generic type we specify it's Output and Failure type. The output type defines what kind of values the subject will send, in our case Int values. Since in cannot fail in our example, we use Never as the error type.
  1. We define the init method with a likesCount parameter to give likesCountSubject an initial value.
  1. We define a publisher named likesCountPublisher of type AnyPublisher<Int, Never>. It has the same Output and Failure type as our subject. The publisher can be used by subscribers. We don't necessarily need the publisher here, we could simply make our subject public. But since we don't want anybody else outside of the article struct sending new values, we use a publisher to only make the subscribing part available to the outside world.
  1. We define the addLike method for increasing the count of likes. Here, we use our subject to send new values. To be able to add a like we access the subject's value property to get the current value of likes. This is the reason why we used a CurrentValueSubject and not a PassthroughSubject in this case, since only a CurrentValueSubject has access to it's current value.

Subscribers

Now, we can use the Article struct as following:

// 1.
let article = Article(likesCount: 5)

// 2.
let likesCountSubscriber = article.likesCountPublisher
    .sink { value in
        print(value)
    }

// 3.
article.addLike()
article.addLike()

Let's go through this code step by step.

  1. We create an article and give it an initial likes count of 5.
  1. We create a subscriber named likesCountSubscriber which is interested in any update on the likes count. The subscriber uses the publisher and it's sink(receiveValue:) method to subscribe for updates. Now, every time the likes count changes, the closure with be called that prints the new value.
  1. We increase the count of likes by calling addLike() twice.

Therefore, the code produces the following output:

5
6
7

When we create a new subscriber, the publisher always returns an object that conforms to the Cancellable protocol. So if we wanted to cancel receiving new values, we could call the cancel() method on the subscriber.

For example, if we add the cancel call in between increasing likes,

article.addLike()
likesCountSubscriber.cancel()
article.addLike()

the last value would not be printed.

Operators

Now, we can use operators to modify the received values before the printing closure is executed. For example, if we want to print something more descriptive, we can use the map operator to map the Int values to String values:

let likesCountSubscriber = article.likesCountPublisher
    .map { "\($0) likes" }
    .sink { print($0) }

Now, our output looks likes this:

5 likes
6 likes
7 likes

Combine provides a lot of useful operators and we can chain as many operators as we want.

To dive deeper into the operators, let's look at the following 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.
    .map { $0.sorted() } // 6.
    .sink { print($0) }

publisher1.send([1, 3])
publisher2.send([6, 10])
publisher2.send([4, 19, 8])
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)

The example doesn't do something useful, but it hopefully gives a good understanding for the operators and combinations of them.

Try to figure out by yourself which output every operator produces before looking into the solution below.

  1. produces [1, 3] [6, 10] [4, 19, 8, 6], because we combine elements from publisher1 with those from publisher2 with the merge operator
  2. produces 1, 3, 6, 10, 4, 19, 8, because we flatten the Int array into a sequence of Int values with flatMap
  3. produces 6, 10, 4, 8, because we filter out all uneven values with the filter operator
  4. produces 10, 4, 8 , because we drop the first element with the dropFirst operator
  5. produces [10, 4, 8], because we collect all received items with the collect operator which returns them as an array
  6. produces [4, 8, 10], because we map the array into a sorted array with the map operator

Of course, there are a lot more operators to discover. The full list of operators is available at the official Publisher documentation.

Memory management in Combine

The lifecycle of a subscriber is linked to the lifecycle of the retaining object. Whenever this object is released, the cancel method is automatically called on the subscriber property and it will be released as well.

Of course, just like with "traditional" memory management in Swift, you need to be aware of not creating retain cycles, e.g. when using strong selfs in the sink closure.

Further reading

Now that you know the Combine basics, my suggestion for the next step would be to look into property wrappers. Especially interesting in this case is the @Publish property wrapper that turns a variable into a Combine publisher.

Written by

Image of the author.
Natascha Fadeeva
Author and creator of this site

Contact

Image Credits

Image credits to Krzysztof Dzwonek from Pixabay