/ IOS

Swift Combine Framework Tutorial: Getting Started

The goal of this article is to paint the big picture of how to use Combine framework in Swift.

What is Combine

Combine is Swift declarative framework for processing values over time [1]. It imposes functional reactive paradigm of programming, which is different from the object-oriented one that prevails in iOS development community.

Reactive means programming with asynchronous streams of values. The Reactive Manifesto tells more about it.

Functional programming is all about programming with functions. In Swift functions can be passed as arguments to other functions, returned from functions, stored in variables and data structures and built at run time as closures.

In declarative programming style you describe what the program does, without describing the flow of control. In imperative style you write how the program works by implementing and handling a series of tasks. Imperative programs rely primarily on state, which is usually modified with assignments.

Programming with Swift Combine framework is both declarative, reactive and functional. It involves chaining functions and passing values from one to the other. This creates the streams of values, flowing from the input to the output.

If we cut all the extra words, Combine is this:

Introduction to Swift Combine Framework

And even shorter:

Combine = Publishers + Subscribers + Operators

What is Publisher

Publisher sends sequences of values over time to one or more Subscribers.

Combine publishers conform to the following protocol:

protocol Publisher {
    associatedtype Output
    associatedtype Failure : Error
    func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

A publisher can send values or terminate with either success or error. Output defines what kind of values a publisher can send. Failure defines the type of error it may fail with.

The receive(subscriber:) method connects a subscriber to a publisher. It defines the contract: publisher’s output must match subscriber’s input, and so do the failure types.

What is Subscriber

Subscriber receives values from a publisher.

Combine subscribers conform to the following protocol:

public protocol Subscriber : CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure : Error

    func receive(subscription: Subscription)
    func receive(_ input: Self.Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Self.Failure>)
}

A subscriber can receive a value of type Input or a termination event with either success or Failure .

The three receive methods describe different steps of the subscriber’s life cycle. We’ll get back to that topic in a couple of paragraphs.

Connecting Publisher to Subscriber

Combine has two built-in subscribers: Subscribers.Sink and Subscribers.Assign. You can connect them by calling either of these methods on a publisher :

  • sink(receiveCompletion:receiveValue:) to handle new element or completion event in a closure.
  • assign(to:on:) to write new element to a property.
// 1
let publisher = Just(1)
// 2
publisher.sink(receiveCompletion: { _ in
    print("finished")
}, receiveValue: { value in
    print(value)
})
  1. Create a Just publisher that sends a single value and then completes. Combine has a number of built-in publishers, including Just.
  2. Connect a Subscribers.Sink subscriber.

It will print:

1
finished

After sending 1, the publisher automatically finishes. We do not have to handle any errors, since Just cannot fail.

What are Subjects

Subject is a special kind of Publisher that can insert values, passed from the outside, into the stream. Subject’s interface provides three different ways of sending elements:

public protocol Subject : AnyObject, Publisher {
    func send(_ value: Self.Output)
    func send(completion: Subscribers.Completion<Self.Failure>)
    func send(subscription: Subscription)
}

Combine has two built-in subjects: PassthroughSubject and CurrentValueSubject.

// 1
let subject = PassthroughSubject<String, Never>()
// 2
subject.sink(receiveCompletion: { _ in
    print("finished")
}, receiveValue: { value in
    print(value)
})
// 3
subject.send("Hello,")
subject.send("World!")
subject.send(completion: .finished) // 4
  1. Create a passthrough subject. We set Failure type to Never to indicate that it always ends successfully.
  2. Subscribe to the subject (remember, it’s still a publisher).
  3. Send two values to the stream and then complete it.

It will print:

Hello,
World!
finished

CurrentValueSubject starts with an initial value and publishes all it’s changes. It can return it’s current value via the value property.

// 1
let subject = CurrentValueSubject<Int, Never>(1)
// 2
print(subject.value)
// 3
subject.send(2)
print(subject.value)
// 4
subject.sink { value in
    print(value)
}
  1. Create a subject with initial value 1.
  2. Access the current value.
  3. Update the current value to 2.
  4. Subscribe to the publisher.

Which will print:

1
2
2

Combine Publisher and Subscriber Life Cycle

A connection between a publisher and a subscriber is called subscription. The steps of such a connection define a publisher-subscriber life cycle.

let subject = PassthroughSubject<Int, Never>()

let token = subject
    .print()
    .sink(receiveValue: { print("received by subscriber: \($0)") })

subject.send(1)

Notice the print(_:to:) operator in the above snippet. It prints log messages for all publishing events to console, which already can tell us a lot about the life cycle.

Here is what gets printed to console:

1
2
3
4
5
receive subscription: (PassthroughSubject)
request unlimited
receive value: (1)
received by subscriber: 1
receive cancel

This gives us a clue about publisher-subscriber life cycle. With some steps missing from the debug log, let’s examine the end to end life cycle:

  1. A subscriber connects to a publisher by calling subscribe<S>(S).
  2. The publisher creates a subscription by calling receive<S>(subscriber: S) on itself.
  3. The publisher acknowledges the subscription request. It calls receive(subscription:) on the subscriber.
  4. The subscriber requests a number of elements it wants to receive. It calls request(:) on the subscription and passes Demand as a parameter. Demand defines how many items a publisher may send to a subscriber via the subscription. In our case, the demand is unlimited.
  5. The publisher sends values by calling receive(_:) on the subscriber. This method returns a Demand instance, which shows how many more items the subscriber expects to receive. The subscriber can only increase the demand or leave it the same, but cannot reduce it.
  6. The subscription ends with one of these outcomes:
    • Cancelled. This can happen automatically when the subscriber is released, which is shown in the above example. Another way is to cancel manually: token.cancel().
    • Finish successfully.
    • Fail with error.

Token is actually the subscriber, type erased to AnyCancellable protocol.

Chaining Publishers with Operators

Operators are special methods that are called on Publishers and return another Publisher. This allows to apply them one after the other, creating a chain. Each operator transforms the publisher, returned by the previous operator.

A chain must be originated by a publisher. Then operators can be applied in turn. Each operator receives the publisher created by the previous operator in the chain. We refer to their relative order as Upstream and Downstream, meaning the immediately previous and next operator.

Let’s see how we can chain operators when handling an HTTP URL request with Combine Swift framework.

// 1
let url = URL(string: "https://api.github.com/users/V8tr/repos")!

// 2
let token = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data } // 3
    .decode(type: [Repository].self, decoder: JSONDecoder()) // 4
    .sink(receiveCompletion: { completion in // 5
        print(completion)
    }, receiveValue: { repositories in
        print("V8tr has \(repositories.count) repositories")
    })
  1. Create the request to load my GitHub repositories. We are using GitHub REST API.
  2. Combine is well-integrated into Swift system frameworks and iOS SDK. This enables us to use a built-in publisher for handling URLSession data tasks.
  3. Pass the response data. We use map(_:) operator, which transforms the upstream value from (data: Data, response: URLResponse) to Data.
  4. Decode the content of response with JSONDecoder.
  5. Connect the sink subscriber. It prints the number of received repositories and the completion.

It will print:

V8tr has 30 repositories
finished

Where to Go From Here?

If you haven’t already, I recommend watching WWDC sessions on Combine:

If you found this article useful, I have some more for you:

Additionally, I recommend reading Reactive Streams specification which is a standard for asynchronous stream processing. It allows to better understand the core Combine concepts: publishers, subscribers and subscriptions.