Combine Introductions

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: PublishersOperators, and Subscribers.

What Are Publishers?

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:

  1. An output value of the publisher’s generic Output type.
  2. A successfull completion
  3. 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?

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:

  1. Sink: Handles received values and completion.
  2. 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

  1. 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.
  2. Where It’s Used:
    • When subscribing to publishers using operators like .sink or .assign, Combine returns a Cancellableinstance.
  3. How It Works:
    • When you cancel a Cancellable, the publisher stops emitting values to its subscriber.

Types of Cancellables

  1. AnyCancellable:
    • A concrete implementation of the Cancellable protocol.
    • Most commonly used and returned by Combine operators like .sink.
  2. Set of Cancellables:
    • Combine provides a convenient way to manage multiple Cancellable instances using a Set<AnyCancellable>.

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?

  1. Memory Management:
    • Prevent memory leaks by ensuring subscriptions don’t outlive their purpose.
    • Automatically clean up resources when Cancellables are deallocated.
  2. Dynamic Unsubscribing:
    • Stop receiving updates from publishers when they are no longer needed, saving processing resources.

Best Practices

  1. Use a Set<AnyCancellable> to store multiple subscriptions in long-lived objects, like view models or controllers.
  2. 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.