In iOS 13/macOS Catalina, however, Apple brought reactive programming support to its ecosystem via the build in system framework combine.
Combine is Apple’s framework for managing asynchronous events in a declarative way. It allows you to process values over time, such as network responses, user input, or even timer updates. The framework relies on three main components: Publishers, Operators, and Subscribers.
What Are Publishers?
A Publisher is a component that emits a sequence of values over time. It defines how values or errors are produced and made available to subscribers. Publishers are used to represent data streams, such as:
- Notifications
- User interactions (e.g., button taps)
- Network responses
- Timer events
import Combine
let publisher = Just("Hello, Combine!")
publisher.sink { value in
print(value) // Outputs: Hello, Combine!
}
Every publisher can emit multiple events of these three types:
- An output value of the publisher’s generic Output type.
- A successfull completion
- A completion with an error of the publisher’s Failure type.
The publisher protocol is generic over two types,
Publisher.Output is the type of the output values of the publisher. If the publisher is specialized as an Int, it can never emit a String or Date value.
Publisher.Failer is the type of error the publisher can throw if it fails, If the publisher can never fail, you specify that by using never failure type.
What Are Operators?
Operators are methods that transform, filter, or manipulate the values emitted by publishers. They allow you to create complex data pipelines while keeping your code declarative and clean.
Common Operators:
map
: Transforms each value.filter
: Filters out unwanted values.flatMap
: Flattens nested publishers.combineLatest
: Combines multiple publishers.
let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher
publisher
.filter { $0 % 2 == 0 } // Emits only even numbers
.map { $0 * 10 } // Multiplies each value by 10
.sink { print($0) } // Outputs: 20, 40
What Are Subscribers?
A Subscriber receives and reacts to the values emitted by a publisher. Without a subscriber, a publisher won’t emit any values. There are two built-in subscriber types in Combine:
- Sink: Handles received values and completion.
- Assign: Assigns values to a property of an object.
class Example {
var value: String = "" {
didSet {
print("Value updated to: \(value)")
}
}
}
let example = Example()
let publisher = Just("Updated Value")
publisher
.assign(to: \.value, on: example)
// Outputs: Value updated to: Updated Value
Publishers do not emit any values if there are no subscribers to potentially receive the output.
Subscriptions are a wonderfull concept in that they allow you to declare a chain of asysnchronous events with their own custom code and error handling only once, and then you never have to think about it again.
Combining Publishers, Operators, and Subscribers
Now that we understand the individual components, let’s see how they work together.
Example: Real-World Scenario Suppose you’re building a feature where you process user input, filter invalid entries, and display the result.
import Combine
// A PassthroughSubject acts as a dynamic publisher
let userInput = PassthroughSubject<String, Never>()
let subscription = userInput
.filter { !$0.isEmpty } // Filter out empty strings
.map { $0.uppercased() } // Transform input to uppercase
.sink { print("Processed Input: \($0)") } // Print the result
// Send events to the publisher
userInput.send("hello")
userInput.send("")
userInput.send("combine")
// Outputs:
// Processed Input: HELLO
// Processed Input: COMBINE
Key Points About Cancellable
- What It Does:
- Keeps the subscription alive as long as the
Cancellable
instance exists. - Cancels the subscription when the
Cancellable
is deallocated or explicitly canceled.
- Keeps the subscription alive as long as the
- Where It’s Used:
- When subscribing to publishers using operators like
.sink
or.assign
, Combine returns aCancellable
instance.
- When subscribing to publishers using operators like
- How It Works:
- When you cancel a
Cancellable
, the publisher stops emitting values to its subscriber.
- When you cancel a
Types of Cancellables
- AnyCancellable:
- A concrete implementation of the
Cancellable
protocol. - Most commonly used and returned by Combine operators like
.sink
.
- A concrete implementation of the
- Set of Cancellables:
- Combine provides a convenient way to manage multiple
Cancellable
instances using aSet<AnyCancellable>
.
- Combine provides a convenient way to manage multiple
Example: Using Cancellable
Here’s an example that demonstrates creating a subscription and canceling it:
import Combine
let publisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
// Store the subscription
let cancellable = publisher
.sink { time in
print("Time: \(time)")
}
// Cancel the subscription after 5 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
cancellable.cancel()
print("Subscription canceled.")
}
Output:
- The timer emits values every second.
- After 5 seconds, the subscription is canceled, and no more values are received.
Managing Multiple Cancellables
For complex scenarios, you might have multiple subscriptions. Combine provides a Set<AnyCancellable>
to manage them efficiently.
Example: Storing Cancellables in a Set
var cancellables = Set<AnyCancellable>()
let publisher1 = Just("Hello")
let publisher2 = Just("Combine")
publisher1
.sink { print($0) }
.store(in: &cancellables)
publisher2
.sink { print($0) }
.store(in: &cancellables)
// Outputs:
// Hello
// Combine
By calling .store(in:)
, the AnyCancellable
instances are automatically managed in the set.When the cancellables
set is deallocated, all associated subscriptions are canceled.
Why Use Cancellable?
- Memory Management:
- Prevent memory leaks by ensuring subscriptions don’t outlive their purpose.
- Automatically clean up resources when
Cancellables
are deallocated.
- Dynamic Unsubscribing:
- Stop receiving updates from publishers when they are no longer needed, saving processing resources.
Best Practices
- Use a
Set<AnyCancellable>
to store multiple subscriptions in long-lived objects, like view models or controllers. - Cancel subscriptions explicitly when they are tied to temporary events or tasks.
By understanding how Cancellable
works, you gain precise control over the lifecycle of your subscriptions, making your Combine-based code more robust and resource-efficient.