Sign in with GitHub to watch this episode for free.

Combine Essentials

Merging Publishers With Combine's Merge Operator

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.

Merging Publishers With Combine's Merge Operator

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.