Getting started with Combine

7 min readOct 20, 2024

In 2019, Apple developed SwiftUI, a reactive approach to designing views for all Apple platforms. This also meant that they implemented a framework under the hood that would make it possible for SwiftUI to provide the changes in values for developers to react to, and SwiftUI views use this framework to react to these changes and update itself

This framework is nothing but Combine which also got released during the same WWDC event along with SwiftUI in 2019. By publishing Combine, Apple provided the developers with an option to write their code in a reactive way, where you define how the program reacts to a particular change of value or event. If you are aware of SwiftUI you must have come across @State property, any changes to which results in the view to refresh itself and display new or modified view based on the state property value. This is Combine at work under the hood in SwiftUI.

Before Combine, RxSwift was a popular and most used reactive framework that was used by developers to write reactive code for UIKit applications. But now since Combine is here it makes more sense to integrate Combine in the new applications, since Combine is already integrated into most of the Swift, UIKit, and SwiftUI components already make it much simpler and more robust to use. Will see such components down the line, but first let’s see what combine is and how it works!

Creating a publisher.

Combine is a framework that allows you to subscribe to the events and then react to them whenever they are received. This event would either contain a value of a predefined type or some error (also known as a failure event) or a completion event. These events are asynchronous, which means you cannot guarantee when will you receive these events over some time. Since you are subscribing to events there must be some mechanism to generate these events, this is the exact job of the Publisher, they generate the events either of predefined type or failure or completed, to which the Subscriber subscribes. The act of a subscriber subscribing to a publisher and making a connection is called a Subscription. There is no limit to how many subscribers can subscribe from a single publisher, but there must be at least one for the Publisher to emit an event, if not then the Publisher will not emit any event.

Now let us see a simple Publisher and subscriber in action.

var subscriptions = Set<AnyCancellable>()
let events = [1,2,3,4,5,6,7,8,9].publisher

events
.sink { print("Recieved value: \\($0)") } //subsribing to the publisher
.store(in: &subscriptions) //storing the subscription.

Many of the Apple frameworks already adapt to combine, making it easy to use them as publishers. In the above example, you can see we are using a publisher that is made out of a simple swift collection. This will give us value over time when we subscribe to it. Here is the output for the above example.

Recieved value: 1
Recieved value: 2
Recieved value: 3
Recieved value: 4
Recieved value: 5
Recieved value: 6
Recieved value: 7
Recieved value: 8
Recieved value: 9

Subscribing to publisher.

Combine offers you two ways to subscribe to any publisher in two ways one is a sink and a second assign let's see both in detail

sink: sink gives you the values directly that were emitted by the publisher and you can use these values to perform some operation as you wish. There is also one more variant of sink block that provides you with two completion blocks as follows

func sink(receiveCompletion: ((Subscribers.Completion<Self.Failure>) -> Void),
receiveValue: ((Self.Output) -> Void))

The first is called the completion block which gives of object of type Completion, this is a simple enum with two values finished and failure(Failure) where Failure confirms to swift Error type. you can user sink with completion and failure as follows.

var subscriptions = Set<AnyCancellable>()
let events = [1,2,3,4,5,6,7,8,9].publisher

events
.sink { completion in
switch completion {
case .finished:
print("Recieved completed event")
case .failure(let error):
print("Recieved error: \\(error.localizedDescription)")
}
} receiveValue: {
print("Recieved value: \\($0)")
}
.store(in: &subscriptions) //storing the subscription.

.finished is a special kind of event that indicates that the publisher has completed publishing all its events and won't be publishing anymore, once received subscriber then unsubscribes itself from the publisher.

.failure(let error) subscriber receives this when the publisher throws any error, here you can catch the error and handle it or re-throw it. In this case also there wont be anymore events published by the publisher.

Here is the output for the above example.

Recieved value: 1
Recieved value: 2
Recieved value: 3
Recieved value: 4
Recieved value: 5
Recieved value: 6
Recieved value: 7
Recieved value: 8
Recieved value: 9
Recieved completed event

assign: enable you to bind the output of one publisher as input to another publisher, or directly assign it to some property of your model or view. There is one catch though, to the user assign you must make sure that the type of the publisher error is Never, which means that the publisher will never throw an error. You can simply turn the error type of publisher into Never by using catch(_:) or replaceError(with:) operators. assign also provides you with two variations, let look at them one at a time

.assign(to: &$some_published_property_on_self)

this is concise version that allows you to assign the published value to a published property of an object. You must reference the published property with & sign followed by $ for binding purpose

.assign(to: \.some_writable_keypath, on: some_object)

this is a more flexible version of the assign operator operator that allows you to assign the published value to some writable properties of the class which may be a published property or non-published property, have a look at below sample code below to make things more clear

class User: ObservableObject {
@Published var name: String = ""
var address: String = ""
}
class UserViewModel {
let user = User()
private var cancellables = Set<AnyCancellable>()

func nameUser () {
Just("John Appleseed")
.eraseToAnyPublisher()
.assign(to: \\.name, on: user)
.store(in: &cancellables)
}

func updateAddress () {
Just("California, U.S.A")
.eraseToAnyPublisher()
.assign(to: \\.address, on: user)
.store(in: &cancellables)
}
}

Storing and canceling subscriptions

Consider the following code

class UserViewModel {
@Published var user = User()

func nameUser () {
userClient.fetchUser()
.eraseToAnyPublisher()
.assign(to: &$user)
}
}

What happens when this function is called? We have created the subscription to the Publisher returned by userClient.fetchUser assigned it to some user property and exited the function. If you write such code it's not gonna work, since subscribers are reference type the subscription will be canceled as soon as you exit the scope of the function as the reference count to this subscription is 0. To avoid this we need to store this subscription, we can do this by updating the above code as follows

class UserViewModel {
@Published var user = User()
var sub: AnyCancellable?
func nameUser () {
sub = userClient.fetchUser()
.eraseToAnyPublisher()
.assign(to: &$user)
}
}

Here we are storing the subscription to sub property of UserViewModel, making sure we keep the instance of the subscription alive, as long as we want. We can then cancel the subscription whenever we want using the cancel() method of AnyCancellable. Also, AnyCancellable is smart, it auto cancels the subscription whenever its instance goes out of scope, in the above case say when UserViewModel is deallocated. Combine does provide you a way to directly save the created subscription in AnyCancellable, that is to use the store operator as follows.

class UserViewModel {
@Published var user = User()
private var cancellables = Set<AnyCancellable>()
func nameUser () {
userClient.fetchUser()
.eraseToAnyPublisher()
.assign(to: &$user)
.store(in: &cancellables)
}
}

You must have noticed this in previous examples, the store operator requires you to pass the collection range to store the subscriptions, it's good practice to use Set to avoid duplications.

Operators

Operator gives combine their supernatural powers and where most of the magic takes place, there are tons of operators available for you to use, though you can create custom operators by extending Publisher, in many cases the default ones are sufficient. We are not gonna look at operators in this article, but just gonna see how it works.

Operators are functions on publishers that perform some operation on the value being published and generate new publisher with the resulting output or error, the input to the operator is usually referred to as upstream whereas its output is downstream we can combine various operators one after another to form more complex logic. Let us look at an example

func oddNumberSquare() {
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].publisher

numbers
.filter { $0 % 2 != 0 }
.map { $0 * $0 }
.sink(receiveValue: { squared in
print("Squared Odd Number: \\(squared)")
})
.store(in: &some_cancellables)
}

In the above example, we are using two operators .filter and .map, as you can see the operator's names are the same as Swift’s standard library, and that is the case for most operators making it easy to understand them. the filter operator simply filters the even numbers passed by the publisher, and passes them to the map operator as a new publisher, which then calculates its square and passes down further, finally getting printed using the sink operator. Like I said earlier there are tons of operators provided by Apple which you can check out here.

In summary, Combine offers a powerful and flexible approach to handling asynchronous events in Swift. By leveraging its various operators, you can create complex logic that simplifies your code and enhances maintainability. Whether you’re dealing with network requests, user input, or complex data transformations, Combine can help you manage these tasks more efficiently. I encourage you to dive into Combine and start integrating it into your projects; you’ll discover new ways to streamline your workflows and build responsive applications.

Also feel free to reach out in case you have any doubts or concerns about this article, either at devsandeshnaik@gmail.com. or on X

--

--

Sandesh Naik
Sandesh Naik

Written by Sandesh Naik

Swift Engineer, Full of dreams

No responses yet