Combine in swift (publisher/subscriber/operator/subject)
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
- Transformation Operators: These operators change the values emitted by the publisher.
- Filtering Operators: These allow values to pass through based on certain conditions.
- Combining Operators: These operators combine multiple publishers into one.
- Timing Operators: These introduce delays, throttle the rate of emissions, or work with timed data.
- 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
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.
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
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
:
- 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 likestruct
orenum
.
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 related to CombineLatest.
Important combine tutorial link:
GitHub Source Code
https://github.com/Nirajpaul2/Combine-in-Swift