Combine in swift (publisher/subscriber/operator/subject)

Nirajpaul Ios
9 min readMar 9, 2022

--

A unified declarative API for processing values over time. Combine can be used to unify and simplify your code for dealing with things like delegates, notifications, timers, completion blocks, and callbacks.

We know that combine is an inbuilt framework in ios now (WWDC 2019, Minimum target should be ios 13), this avoids the dependency over 3rd party libraries like RxSwift, RxCocoa. same experience without 3rd party library.

This is the framework that works under observation patterns. like we work with KVO, KVC, NSNotificationcenter.

Before deep dive into the combine, let's see that is observation pattern.

Observation pattern:

It is one of the Behavioural Patterns(means communication and interaction b/w the object)

It defines one-many relationship. So when one object changes its state, its dependents are notified.

Pub Sub Pattern (Publisher Subscriber pattern)

Problems solved by observation pattern

Only the subscriber will get its information from the publisher.

Example: (YouTube subscription example) Only the person who subscribes to the channel, will get the latest video content. The person who is not a subscriber, will not get video content.

Combine basics

The main component of the combines are publishers, operators, subscribers, and Subjects.

Publishers

Responsibility to publish the data to the subscribers.

A Publisher can have so many subscribers.

Types of Publishers:

In Combine, there are many built-in publishers, but you can also create custom publishers. Below are some common types of publishers:

Just Publisher: Emits exactly one value and then completes.

let publisher = Just("Hello, Combine!")

Empty Publisher: Does not emit any values but completes immediately.

let publisher = Empty<String, Never>()

Future Publisher: Emits a single value or an error at some point in the future.

let future = Future<Int, Error> { promise in 
promise(.success(42))
}

Fail Publisher: Emits an error and does not emit any values.

let failurePublisher = Fail(outputType: String.self, failure: MyError.someError)

Timer Publisher: Emits a sequence of values based on a timer interval.

let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

NotificationCenter Publisher: Publishes values when a Notification is posted.

let publisher = NotificationCenter.default.publisher(for: .myNotification)

URLSession Publisher: Publishes the result of a network request.

let publisher = URLSession.shared.dataTaskPublisher(for: myURL)

Subscribers

Subscribers are methods with closures, where we access the output values.

Example: sink, assign

Operator

In the Combine framework, operators are functions that allow you to manipulate, transform, and combine data streams emitted by publishers.

Key Concepts of Operators:

  • Chaining: You can chain multiple operators together to create complex data flows. Each operator takes the output from the previous publisher and produces a modified result for the next.
  • Non-destructive: Operators do not modify the original publisher but instead return a new publisher with the applied transformations.

Common Categories of Combine Operators

  1. Transformation Operators: These operators change the values emitted by the publisher.
  2. Filtering Operators: These allow values to pass through based on certain conditions.
  3. Combining Operators: These operators combine multiple publishers into one.
  4. Timing Operators: These introduce delays, throttle the rate of emissions, or work with timed data.
  5. Error Handling Operators: These manage errors emitted by publishers.

1. Transformation Operators

These operators modify or transform the emitted values.

a) map(_:)

Transforms the values emitted by the publisher by applying a function.

let publisher = Just(2)
.map { $0 * 2 } // Multiply each value by 2

let cancellable = publisher.sink { value in
print(value) // Output: 4
}

b) flatMap(_:)

Transforms the emitted value into a new publisher and flattens the result into a single stream.

let publisher = Just("Hello")
.flatMap { _ in Just("World") }

let cancellable = publisher.sink { value in
print(value) // Output: "World"
}

c) scan(_:_:_)

Accumulates a value across multiple emissions, similar to reduce, but emits intermediate results.

let numbers = [1, 2, 3, 4]
let publisher = numbers.publisher
.scan(0) { $0 + $1 } // Running sum

let cancellable = publisher.sink { value in
print(value)
}
// Output: 1, 3, 6, 10

2. Filtering Operators

These operators allow values to pass based on specific conditions.

a) filter(_:)

Passes only the values that satisfy a predicate.

let publisher = [1, 2, 3, 4, 5].publisher
.filter { $0 % 2 == 0 } // Pass even numbers only

let cancellable = publisher.sink { value in
print(value) // Output: 2, 4
}

b) removeDuplicates()

Suppresses consecutive duplicate values.

let publisher = [1, 1, 2, 2, 3].publisher
.removeDuplicates()

let cancellable = publisher.sink { value in
print(value) // Output: 1, 2, 3
}

c) compactMap(_:)

Transforms the values, but ignores nil values.

let publisher = ["1", "a", "3"].publisher
.compactMap { Int($0) } // Converts strings to Int, ignoring invalid ones

let cancellable = publisher.sink { value in
print(value) // Output: 1, 3
}

3. Combining Operators

These operators combine multiple publishers into a single one.

a) combineLatest(_:)

Combines the latest values from two or more publishers. When any publisher emits a new value, the combined publisher emits a tuple containing the latest values from each.

let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

let cancellable = publisher1.combineLatest(publisher2)
.sink { (string, number) in
print("\(string) - \(number)")
}
publisher1.send("A") // No output yet, waiting for both publishers
publisher2.send(1) // Output: A - 1
publisher1.send("B") // Output: B - 1

b) merge(with:)

Merges multiple publishers into a single stream, emitting values as they arrive from any publisher.

let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<String, Never>()

let cancellable = publisher1
.merge(with: publisher2)
.sink { value in
print(value)
}
publisher1.send("Hello") // Output: "Hello"
publisher2.send("World") // Output: "World"

c) zip(_:)

Combines values from two publishers into pairs, but only when both publishers have emitted a value.

let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

let cancellable = publisher1.zip(publisher2)
.sink { (string, number) in
print("\(string) - \(number)")
}
publisher1.send("A")
publisher2.send(1) // Output: A - 1
publisher1.send("B")
publisher2.send(2) // Output: B - 2

4. Timing Operators

These operators manage the timing of emissions.

a) delay(for:tolerance:scheduler:)

Delays the delivery of values by a specified time interval.

let publisher = Just("Delayed message")
.delay(for: .seconds(2), scheduler: RunLoop.main)

let cancellable = publisher.sink { value in
print(value) // Output after 2 seconds: "Delayed message"
}

b) debounce(for:scheduler:)

Waits for a pause in the emissions of values and then delivers the last value emitted during that period.

let publisher = PassthroughSubject<String, Never>()
let cancellable = publisher
.debounce(for: .seconds(1), scheduler: RunLoop.main)
.sink { value in
print(value)
}

publisher.send("A") // No output yet, waiting for pause
publisher.send("B") // Still no output
// Output "B" after 1 second pause

c) throttle(for:scheduler:latest:)

Emits a value from the upstream publisher at most once within the specified time interval.

let publisher = PassthroughSubject<String, Never>()

let cancellable = publisher
.throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true)
.sink { value in
print(value)
}

publisher.send("A") // Output: A
publisher.send("B") // No output yet
// Output "B" after 1 second

5. Error Handling Operators

These operators help handle or recover from errors emitted by publishers.

a) catch(_:)

Replaces an error with another publisher.

let failingPublisher = Fail<String, Error>(error: NSError(domain: "", code: -1))
let cancellable = failingPublisher
.catch { _ in Just("Recovered") }
.sink { value in
print(value) // Output: "Recovered"
}

b) retry(_:)

Attempts to re-subscribe to a publisher if it fails, retrying a specified number of times.

let publisher = Fail<String, Error>(error: NSError(domain: "", code: -1))
.retry(2)
let cancellable = publisher
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
// Output: Prints completion after 2 retries

There is so many operators are available: https://developer.apple.com/documentation/combine/publisher

Publisher Subscriber lifecycle

publisher subscriber lifecycle
Pub-Sub life cycle

We can see a function pubSubLifeCycle(), Points

A: We create a Just publisher to perform adding.

B: We create a Just subscriber to get its value.

C: We add the justSubscriber to justPublisher and add print() with publisher, that will print the whole lifecycle of the pub-sub, we can see in the console.

Publisher in detail with an example:

Just Publisher

A publisher that emits an output to each subscriber just once, and then finishes. and only one element a publisher emits.

Just Publisher
Just Publisher

Future Publisher

Future is a protocol.

Future can be used to asynchronously produce a single result and then complete.

It is invoked in the future when an element or error is available.

Based on promise: The promise closure receives one parameter: a `Result` that contains either a single element published by a ``Future``, or an error.

DataTaskPublisher call inside the Future publisher

DataTaskPublisher call inside the Future publisher

Subscriber

Subscribers are methods with closures, where we access the output values.

sink:

The sink subscriber allows you to provide closures with your code that will receive output values and completions.

Assign:

Key Points About assign:

  1. Reference Types Only:
  • assign works only with reference types, i.e., objects that are instances of classes. This is because it needs a mutable reference to update the property.
  • You cannot use assign with value types like struct or enum.

2. Properties Must Be Writable:

  • The property you’re assigning the values to must be a writable property, meaning it can’t be a let constant.
  • If you want automatic updates on properties, you often use properties marked with @Published.

Example 1: Binding a Publisher to a Property

Let’s look at a simple example where we update a property using assign.

Example 2: Using @Published with assign

In a real-world application, you often use @Published to make properties observable, especially in SwiftUI or when using Combine with UIKit.

Difference b/w assign and sink

  • sink: Use when you need custom logic for handling values and completions. similar to KVC.
  • assign: Use when you want to automatically assign a value to a property without extra logic.

Subject

A publisher that exposes a method for outside callers to publish elements.

PassthroughSubject = A doorbell push button

When someone rings the door, you are notified only if you are at home (you are the subscriber)

PassthroughSubject doesn’t have a state, it emits whatever it receives to its subscribers.

CurrentValueSubject = A light switch Someone turns on the lights in your home when you are outside. You get back home and you know someone has turned them on.

CurrentValueSubject has an initial state, it retains the data.

Example: GitHub link

Operator

Operators are methods, which is a specific functions that allow you to process the incoming data, before send it to subscribers.

EraseToAnyPublisher: Erases a publisher’s generic type to a simple AnyPublisher.

Debounce: Picking individual values over time is easy with debounce,Example: search something

removeDuplicates: Publishes only elements that don’t match the previous element.

Collect: Collects all received elements, and emits a single array of the collection when the upstream publisher finishes.

Map: Use map to loop over a collection and apply the same operation to each element in the collection. The map function returns an array containing the results of applying a mapping or transform function to each item.

[1, 2, 3]
.publisher
.map { $0 * 2 }
.sink { print($0) }
Print
2
4
6

Reduce: Very useful in order to combine the elements of a sequence into a single value.

CombineLatest with accept 2 parameters example

Find GitHub Source Code

Find GitHub Source Code related to CombineLatest.

Important combine tutorial link:

  1. https://www.raywenderlich.com/books
  2. https://www.swiftbysundell.com/basics/combine/

GitHub Source Code
https://github.com/Nirajpaul2/Combine-in-Swift

Thanks for reading!

--

--