This episode of Combine Essentials zooms in on Combine's merge
operator. As the name suggests, the merge
operator merges two or more upstream publishers into a single publisher. Even though the merge
operator isn't difficult to use, there are a few pitfalls to watch out for.
Merging Publishers with the Merge Operator
Because a code snippet is worth a thousand words, let's start with an example. In setupBindings()
, we subscribe to UIApplication.didEnterBackgroundNotification
and UIApplication.willTerminateNotification
notifications. This is easy and convenient thanks to Combine's integration with the Foundation framework. In the handler we pass to the sink(receiveValue:)
method, we invoke save()
on a Core Data manager to push unsaved changes to the persistent store. This is a common pattern in Core Data applications.
private func setupBindings() {
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil)
.sink { [weak self] _ in
self?.coreDataManager.save()
}.store(in: &subscriptions)
NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification, object: nil)
.sink { [weak self] _ in
self?.coreDataManager.save()
}.store(in: &subscriptions)
}
You may notice that the implementation suffers from code duplication. This is easy to resolve using Combine's merge
operator. To keep the implementation readable, we store a reference to each publisher in a constant, didEnterBackground
and willTerminate
.
private func setupBindings() {
let didEnterBackground = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil)
let willTerminate = NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification, object: nil)
}
We invoke the merge(with:)
method on the didEnterBackground
publisher, passing in the willTerminate
publisher as an argument. The publisher the merge
operator returns merges the elements emitted by the upstream publishers. We can subscribe to the resulting publisher by invoking the sink(receiveValue:)
method. In the handler we pass to the sink(receiveValue:)
method, we invoke save()
on the Core Data manager to push unsaved changes to the persistent store. That's it.
private func setupBindings() {
let didEnterBackground = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil)
let willTerminate = NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification, object: nil)
didEnterBackground.merge(with: willTerminate)
.sink(receiveValue: { [weak self] notification in
self?.coreDataManager.save()
}).store(in: &subscriptions)
}
The result is identical if we were to invoke merge(with:)
on the willTerminate
publisher, passing in the didEnterBackground
publisher as an argument.
private func setupBindings() {
let didEnterBackground = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil)
let willTerminate = NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification, object: nil)
willTerminate.merge(with: didEnterBackground)
.sink(receiveValue: { [weak self] notification in
self?.coreDataManager.save()
}).store(in: &subscriptions)
}
There is another API we can use to merge publishers. Publishers
is a namespace for types that serve as publishers and it defines the Merge
struct. The Merge
struct conforms to the Publisher
protocol, which means it is a publisher.
The initializer of the Merge
struct accepts two publishers. Because Publishers.Merge
is a publisher itself, we can subscribe to the publisher like we did earlier in this episode.
private func setupBindings() {
let didEnterBackground = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil)
let willTerminate = NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification, object: nil)
Publishers.Merge(didEnterBackground, willTerminate)
.sink(receiveValue: { [weak self] notification in
self?.coreDataManager.save()
}).store(in: &subscriptions)
}
Matching Output and Failure Types
It is important to understand that the upstream publishers that are merged need to have matching Output
and Failure
types. The signature of the merge(with:)
method confirms this. The method signature also shows that the Output
and Failure
types of the upstream publishers define the Output
and Failure
type of the resulting publisher.
public func merge<P>(with other: P) -> Publishers.Merge<Self, P> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output
The compiler throws an error if you violate this requirement. This requirement makes sense since the publisher the merge
operator returns inherits the Output
and Failure
types of the upstream publishers, which means they need to match.
You could work around this limitation by wrapping the elements the publishers emit in a container. While this works, I have never come across a scenario in which this workaround was necessary.
Merging More Than Two Publishers
In the previous examples, you learned how to merge two publishers. It is possible to merge more than two publishers into a single publisher. The Combine framework defines a number of variants of the merge(with:)
method. In this example, we merge four upstream publishers into a single publisher.
private func setupBindings() {
let willEnterForeground = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification, object: nil)
let didEnterBackground = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil)
let didBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification, object: nil)
let willTerminate = NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification, object: nil)
willEnterForeground.merge(with: didEnterBackground, didBecomeActive, willTerminate)
.sink(receiveValue: { [weak self] _ in
self?.coreDataManager.save()
}).store(in: &subscriptions)
}
The Combine framework also defines several variants of the Merge
struct that support merging more than two publishers, Merge2
, Merge3
, Merge4
, Merge5
, Merge6
, etc.
private func setupBindings() {
let willEnterForeground = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification, object: nil)
let didEnterBackground = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil)
let didBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification, object: nil)
let willTerminate = NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification, object: nil)
Publishers.Merge4(willEnterForeground, didEnterBackground, didBecomeActive, willTerminate)
.sink(receiveValue: { [weak self] _ in
self?.coreDataManager.save()
}).store(in: &subscriptions)
}
What's Next?
The merge
operator merges two or more upstream publishers into a single publisher. Keep in mind that the Output
and Failure
types of the upstream publishers need to match. The compiler throws an error if they don't. If you need to combine publishers that have mismatching Output
and/or Failure
types, then you might need to look into the combineLatest
operator.