iOS Combine Framework

8 Oct 2021
8 min read

Before going deep inside the Combine Framework, let’s recall the difference between synchronous and asynchronous programming
With synchronous programming, we’re referring to all those programs written in a single-threaded language that execute sequentially line-by-line. Because of that, we’re always sure about the current state of our variables.
With asynchronous programming, instead, we’re referring to all that programs written in a multi-threaded language that is running on an asynchronous event-driven UI framework like UIKit. In this situation, because more than one thread is executed at the same time, it’s difficult to understand what is causing some errors in the code.

So to solve this type of difficulty apple has introduced a combined framework l. From Apple documentation, we know that: “The Combine framework provides a declarative approach for how your app processes events. Rather than potentially implementing multiple delegate callbacks or completion handler closures, you can create a single processing chain for a given event source. Each part of the chain is a Combine operator that performs a distinct action on the elements received from the previous step. “. 

Why use Combine?

Using Combine is not necessary to make a good app, because everything we’ve seen can be built in a custom way.

But its usage allows you to add another level of abstraction, safe and efficient, to your asynchronous code. This means a better integration that’s well tested and supported.

In fact, usually, asynchronous code testing is very complex, while with Combine you’re going to use operators already tested so that you can only worry about the implementation logic of the app. To these benefits, there is also the fact that Combine isn’t a framework that affects how you structure your app because it doesn’t alter how you would separate responsibilities in your project (For example, MVVM or MVC) and it’s also possible to use it only in some parts of the code.

Basic Elements:

he four key moving pieces in Combine are:

  1. Publishers
  2. Operators
  3. Subscribers
  4. Subjects

We’re now going to have an introduction about all of them, to then focus on Publishers, Subscribers, Subjects and their protocols.

Publishers:

Publishers are types that can emit values over time to one or more interested parties, like the subscribers. Every publisher can emit multiple events of these three types:

  1. An output value of the publisher’s generic Output type
  2. successful completion
  3. completion with an error of the publisher’s Failure type

A publisher can emit zero or more output values and, if it ever completes (Either successfully or due to a failure), it will not emit any other events. Thanks to these three types of events emitted, we can assign to the publisher any kind of task inside the application, from reacting to user gestures to making network calls. One of the publisher’s best features is that they come with error handling built-in. The Publisher protocol is generic over two types:

  1. Publisher .Output: Type of publisher’s output value
  2. Publisher .Failure: Type of the error that the publisher can throw if it fails

When you subscribe to a given publisher, you know what values to expect from it and which errors it could fail with.

Operators:

Operators are methods declared on the Publisher protocol that returns either the same or a new publisher. That is very useful because we can call a bunch of operators one after the other, effectively chaining them together.

These operators can be combined together to implement very complex logic over the execution of a single subscription. It’s impossible to put them in the wrong order because one’s output will not match the next one’s input.

They always have an input and an output, commonly referred to as upstream and downstream, in order to avoid a shared state.
Operators focus on working with the data they receive from the previous operator and on provide their output to the next one. This means that no other asynchronously running piece of code can modify the data they’re working on in the meanwhile.

Subscribers:
At the end of the subscription chain, we found the subscribers. They usually do something with the output or the completion event emitted by the chain. Actually, Combine provide two built-in subscribers:

  • Sink: This type of subscriber allows you to provide closures with your code that will receive output values and completions, which it could then work on.
  • Assign: Allows you to bind the resulting output to some property on your data model or an a UI control.

In case these are not enough, it’s possible to override the Combine protocol to create a custom one.

Publishers & Subscribers: Let see how Publisher and Subscribers work practically Publisher Protocol:

As you can see from the code, the two associated type are the publisher’s interface that a subscriber must match in order to create a subscription. 

One thing you need to understand is that everything in Combine is a Publisher or something that operates on or subscribes to values emitted by a Publisher.

ArraysStrings or Dictionaries can be converted to Publishers in Combine
Let’s create some simple ones.

let stringPublisher = "Hello Combine".publisher()
let arrayPublisher = [1,2,3,5].publisher()
let dictPublisher = [1:"Hello",2:"World"].publisher()

You subscribe to publishers by calling  sink(receiveValue: (value -> Void))
The passed block will receive all values emitted by that publisher.

let stringPublisher = [1,2,3,5].publisher()

_ = stringPublisher.sink { value in
    print(value)
}

OUTPUT: 
1 2 3 5

Publishers can emit zero or more values over their lifetimes.  Besides the basic values, your Publisher also emits special values represented by the Subscribers. Completion enum.

  • .finished will be emitted if the subscription is finished
  • .failure(_) will be emitted if something went wrong

The associated value for the failure case can be a custom Object, an Error or a special Never object that indicates that the Publisher won’t fail.

let stringPublisher = [1,2,3,5].publisher()
_ = stringPublisher.sink(receiveCompletion: { completion in
    switch completion {
        case .finished:
            print("finished")
        case .failure(let never):
            print(never)
    }
}, receiveValue: { value in
    print(value)
})

OUTPUT:
1 2 3 5
finished

If you want to cancel a subscription you can do that by calling cancel on it.

let subscriber = stringPublisher.sink { value in
    print(value)
}

subscriber.cancel()

Using Operators It is also possible to transform or filter the elements emitted by a Publisher before the subscriber receives them.

let stringPublisher = [1,2,3,5].publisher()

_ = stringPublisher.map {
    return $0 * 10
}.sink { value in
    print(value)
}

OUTPUT: 10 20 30 50

Here map is acting as an operator,  we can apply a chain of such operators on the publisher to transform it like a filter, reduce, collect etc.

Subjects :

A Subject is a special form of a Publisher, you can subscribe and dynamically add elements to it. There are currently 2 different kinds of Subjects in Combine

  • PassthroughSubject: If you subscribe to it you will get all the events that will happen after you subscribed.
  • CurrentValueSubject: will give any subscriber the most recent element and everything that is emitted by that sequence after the subscription happened.

PassthroughSubject :

The first thing we need to do is to create an actual PassthroughSubject instance. This is super easy, we can use the default initializer for that.

You can add new Values to that subject by using the send(input:String) function. If you want to complete the sequence you need to use send(completion: .finished).
send(completion: someError)
 will emit an error,   same as previous examples.

let passThroughSubject = PassthroughSubject<String, Error>()
passThroughSubject.send("Hello")
passThroughSubject.sink(receiveValue: { value in
    print(value)
})
passThroughSubject.send("World")


OUTPUT:
World

As you see you will just receive values that were sent after you subscribed to the Subject, not before that,  hence “Hello” was not received but “World” was received

CurrentValueSubject:

Inapposite
to a PassthroughSubjectthe CurrentValueSubject will receive the “Hello”-String, which is the most recent value. So it works a little bit more like an actual variable.

let subject = CurrentValueSubject<String, Error>("Initial Value")
subject.send("Hello")
currentValueSubject.sink(receiveValue: { value in
    print(value)
})

OUTPUT:
Hello


Key points

  • Publishers transmit a sequence of values over time to one or more subscribers, either synchronously or asynchronously.
  • A subscriber can subscribe to a publisher to receive values; however, the subscriber’s input and failure types must match the publisher’s output and failure types.
  • There are two built-in operators you can use to subscribe to publishers: sink(_:_:) and assign(to:on:).
  • Subjects are publishers that enable outside callers to send multiple values asynchronously to subscribers, with or without a starting value.

Conclusion:
The basic principles are explained, and there’s a lot more. The combine is a really nice framework, we should definitively learn it eventually. It’s also a good opportunity to refactor our legacy / callback-based code into a nice modern declarative one. We can simply transform all our old delegates into publishers by using subjects.

I am still learning more advanced concepts in combine framework like Published properties, Creating a custom Publisher etc.., as I progress will post the blog on same.
Also, the following WWDC sessions are a great start to give yourself some more background information:

References:
https://developer.apple.com/documentation/combine
https://developer.apple.com/documentation/combine/publisher
https://developer.apple.com/documentation/combine/subscriber

Discover more from Tech Shaadi

Subscribe now to keep reading and get access to the full archive.

Continue reading