Combine for Swift

Swarandeep Singh Sran
4 min readNov 10, 2024

--

Combine is very vast topic. To better understand it, we will break it into number of articles. So, In this (first) article, we will go through the basics of combile.

According to Apple’s official documentation

The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.
(https://developer.apple.com/documentation/combine)

Before moving forward, lets recap what’s the difference between Declarative and Imperative

Imperative means Step-by-Step approach. With an imperative approach, you manually define the steps to achieve a result.
Code Example:

var numbers = [1, 2, 3, 4, 5]
var result = [Int]()

for number in numbers {
if number > 2 {
result.append(number * 2)
}
}
print(result)

A declarative approach focuses on what you want to accomplish, not the exact steps to do it.

let numbers = [1, 2, 3, 4, 5]
let result = numbers.filter { $0 > 2 }.map { $0 * 2 }
print(result)

Declarative APIs make code more concise, easier to read, and often simpler to maintain. In reactive programming frameworks like Combine, RxSwift, or ReactiveSwift, declarative APIs allow developers to handle complex asynchronous operations in a readable way, without explicitly managing each state change or step in the process.

Publishers and subscribers

Publishers and Subscribers are the two primary components for handling asynchronous data streams

A Publisher is an object that produces a stream of values over time, which other components (Subscribers) can listen to. Publishers define what kind of data they emit and what kind of errors (if any) they can produce. Publishers don’t actively produce values until a Subscriber subscribes to them.

Key Characteristics of Publishers

  1. Types of Publishers: Publishers can produce a single value, multiple values, or even no values, depending on the type of stream or data source.
  2. Type-Safe Data and Errors: Each Publisher has two associated types:
  • Output: The type of data it emits.
  • Failure: The type of error it might produce (if any).

3. Declarative Pipelines: You can apply various Combine operators (e.g., map, filter, debounce) to transform, filter, or process the values in a publisher's stream.

import Combine

// A publisher that emits a single integer value
let numberPublisher = Just(5)

numberPublisher
.map { $0 * 2 } // Transform the value by doubling it
.sink { value in
print("Received value: \(value)") // Output: Received value: 10
}

We can get the list of all the publisher operators in the link

A Subscriber is an object that “subscribes” to a Publisher and receives the values it emits. Subscribers are responsible for defining how to handle incoming data and completion events (success or failure). When a Publisher produces new values, it sends them to its subscribers.

Key Characteristics of Subscribers

  1. Receiving Data: Subscribers receive values from Publishers and define actions to take with those values.
  2. Completion Handling: They handle the end of the stream, which can either be:
  • Finished: The Publisher has completed successfully.
  • Failure: The Publisher has completed with an error.

3. Built-In Subscribers: The Combine framework provides a built-in subscriber called sink, which allows you to define closures for handling values and completion events.

import Combine

let namePublisher = ["Alice", "Bob", "Charlie"].publisher

namePublisher
.filter { $0.starts(with: "A") } // Filter names that start with "A"
.sink(
receiveCompletion: { completion in
print("Stream completed with: \(completion)")
},
receiveValue: { name in
print("Received name: \(name)")
}
)

Relationship Between Publishers and Subscribers

  • Subscription: When a Subscriber subscribes to a Publisher, the Publisher begins emitting values.
  • Data Flow: Data flows from Publisher to Subscriber. This data flow is flexible and can involve transformations, filters, and combinations of multiple Publishers before reaching the Subscriber.
  • Completion: The Publisher notifies the Subscriber when it has no more data (or if it encounters an error).

Operators

Operators are powerful methods you can use to transform, filter, combine, or handle data streams emitted by Publishers. Operators allow you to build a pipeline of transformations, making it easy to manipulate data before it reaches your subscribers.

let numbers = [1, 2, 3, 4, 5].publisher
numbers
.map { $0 * 2 }
.sink { print($0) } // Output: 2, 4, 6, 8, 10

In Combine, you can chain multiple operators together to create a pipeline that processes each value emitted by a Publisher, transforming and filtering the data until it reaches the Subscriber. The combination of operators gives you a powerful toolkit for handling data streams in a reactive, declarative, and highly readable way.

.sink is a method in Combine that attaches a subscriber to a Publisher and allows you to handle emitted values and completion events with custom closures. It’s one of the most commonly used ways to receive and process data from a Publisher, often making it the last step in a Combine pipeline.

When you use .sink, you have the option to define two closures:

  1. Value closure: Processes each value that the Publisher emits.
  2. Completion closure: Handles the completion event, which could be a normal completion (no more values) or a failure (an error occurred).

The .sink method also returns an AnyCancellable instance, which represents the subscription and lets you manage its lifecycle.

import Combine

let publisher = ["A", "B", "C"].publisher

let cancellable = publisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Finished successfully.")
case .failure(let error):
print("Finished with error: \(error)")
}
},
receiveValue: { value in
print("Received value: \(value)")
}
)

// Output:
// Received value: A
// Received value: B
// Received value: C
// Finished successfully.

The .assign operator in Combine is used to bind (or "assign") the output of a publisher to a property on an object. It’s particularly useful when you want to automatically update properties with values from a publisher’s stream without needing a custom sink or explicit subscription handling.

Syntax

publisher
.assign(to: \.property, on: object)

--

--

No responses yet