CombineExt
CombineExt provides a collection of operators, publishers and utilities for Combine, that are not provided by Apple themselves, but are common in other Reactive Frameworks and standards.
Install / Use
/learn @CombineCommunity/CombineExtREADME
CombineExt
<p align="center"> <img src="https://github.com/CombineCommunity/CombineExt/raw/main/Resources/logo.png" width="45%"> <br /><br /> <a href="https://actions-badge.atrox.dev/CombineCommunity/CombineExt/goto" target="_blank" alt="Build Status" title="Build Status"><img src="https://github.com/CombineCommunity/CombineExt/workflows/CombineExt/badge.svg?branch=main" alt="Build Status" title="Build Status"></a> <a href="https://codecov.io/gh/CombineCommunity/CombineExt" target="_blank" alt="Code Coverage for CombineExt on codecov" title="Code Coverage for CombineExt on codecov"><img src="https://codecov.io/gh/CombineCommunity/CombineExt/branch/main/graph/badge.svg" alt="Code Coverage for CombineExt on codecov" title="Code Coverage for CombineExt on codecov"/></a> <br /> <img src="https://img.shields.io/badge/platforms-iOS%2013%20%7C%20macOS 10.15%20%7C%20tvOS%2013%20%7C%20watchOS%206-333333.svg" /> <br /> <a href="https://github.com/apple/swift-package-manager" target="_blank"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" alt="CombineExt supports Swift Package Manager (SPM)"></a> </p>CombineExt provides a collection of operators, publishers and utilities for Combine, that are not provided by Apple themselves, but are common in other Reactive Frameworks and standards.
The original inspiration for many of these additions came from my journey investigating Combine after years of RxSwift and ReactiveX usage.
All operators, utilities and helpers respect Combine's publisher contract, including backpressure.
Operators
- withLatestFrom
- flatMapLatest
- assign
- amb and Collection.amb
- materialize
- values
- failures
- dematerialize
- partition
- zip(with:) and Collection.zip
- Collection.merge()
- combineLatest(with:) and Collection.combineLatest
- mapMany(_:)
- filterMany(_:)
- setOutputType(to:)
- removeAllDuplicates and removeAllDuplicates(by:)
- share(replay:)
- prefix(duration:tolerance:on:options:)
- prefix(while:behavior:)
- toggle()
- nwise(_:) and pairwise()
- ignoreOutput(setOutputType:)
- ignoreFailure
- mapToResult
- flatMapBatches(of:)
Publishers
Subjects
Note: This is still a relatively early version of CombineExt, with much more to be desired. I gladly accept PRs, ideas, opinions, or improvements. Thank you! :)
Installation
Swift Package Manager
Add the following dependency to your Package.swift file:
.package(url: "https://github.com/CombineCommunity/CombineExt.git", from: "1.0.0")
Operators
This section outlines some of the custom operators CombineExt provides.
withLatestFrom
Merges up to four publishers into a single publisher by combining each value from self with the latest value from the other publishers, if any.
let taps = PassthroughSubject<Void, Never>()
let values = CurrentValueSubject<String, Never>("Hello")
taps
.withLatestFrom(values)
.sink(receiveValue: { print("withLatestFrom: \($0)") })
taps.send()
taps.send()
values.send("World!")
taps.send()
Output:
withLatestFrom: Hello
withLatestFrom: Hello
withLatestFrom: World!
flatMapLatest
Transforms an output value into a new publisher, and flattens the stream of events from these multiple upstream publishers to appear as if they were coming from a single stream of events.
Mapping to a new publisher will cancel the subscription to the previous one, keeping only a single subscription active along with its event emissions.
Note: flatMapLatest is a combination of map and switchToLatest.
let trigger = PassthroughSubject<Void, Never>()
trigger
.flatMapLatest { performNetworkRequest() }
trigger.send()
trigger.send() // cancels previous request
trigger.send() // cancels previous request
assign
CombineExt provides custom overloads of assign(to:on:) that let you bind a publisher to multiple keypath targets simultaneously.
var label1: UILabel
var label2: UILabel
var text: UITextField
["hey", "there", "friend"]
.publisher
.assign(to: \.text, on: label1,
and: \.text, on: label2,
and: \.text, on: text)
CombineExt provides an additional overload — assign(to:on:ownership) — which lets you specify the kind of ownersip you want for your assign operation: strong, weak or unowned.
// Retain `self` strongly
subscription = subject.assign(to: \.value, on: self)
subscription = subject.assign(to: \.value, on: self, ownership: .strong)
// Use a `weak` reference to `self`
subscription = subject.assign(to: \.value, on: self, ownership: .weak)
// Use an `unowned` reference to `self`
subscription = subject.assign(to: \.value, on: self, ownership: .unowned)
amb
Amb takes multiple publishers and mirrors the first one to emit an event. You can think of it as a race of publishers, where the first one to emit passes its events, while the others are ignored (there’s also a Collection.amb method to ease working with multiple publishers).
The name amb comes from the Reactive Extensions operator, also known in RxJS as race.
let subject1 = PassthroughSubject<Int, Never>()
let subject2 = PassthroughSubject<Int, Never>()
subject1
.amb(subject2)
.sink(receiveCompletion: { print("amb: completed with \($0)") },
receiveValue: { print("amb: \($0)") })
subject2.send(3) // Since this subject emit first, it becomes the active publisher
subject1.send(1)
subject2.send(6)
subject1.send(8)
subject1.send(7)
subject1.send(completion: .finished)
// Only when subject2 finishes, amb itself finishes as well, since it's the active publisher
subject2.send(completion: .finished)
Output:
amb: 3
amb: 6
amb: completed with .finished
materialize
Convert any publisher to a publisher of its events. Given a Publisher<Output, MyError>, this operator will return a Publisher<Event<Output, MyError>, Never>, which means your failure will actually be a regular value, which makes error handling much simpler in many use cases.
let values = PassthroughSubject<String, MyError>()
enum MyError: Swift.Error {
case ohNo
}
values
.materialize()
.sink(receiveCompletion: { print("materialized: completed with \($0)") },
receiveValue: { print("materialized: \($0)") })
values.send("Hello")
values.send("World")
values.send("What's up?")
values.send(completion: .failure(.ohNo))
Output:
materialize: .value("Hello")
materialize: .value("World")
materialize: .value("What's up?")
materialize: .failure(.ohNo)
materialize: completed with .finished
values
Given a materialized publisher, publish only the emitted upstream values, omitting failures. Given a Publisher<Event<String, MyError>, Never>, this operator will return a Publisher<String, Never>.
Note: This operator only works on publishers that were materialized with the materialize() operator.
let values = PassthroughSubject<String, MyError>()
enum MyError: Swift.Error {
case ohNo
}
values
.materialize()
.values()
.sink(receiveValue: { print("values: \($0)") })
values.send("Hello")
values.send("World")
values.send("What's up?")
values.send(completion: .failure(.ohNo))
Output:
values: "Hello"
values: "World"
values: "What's up?"
failures
Given a materialized publisher, publish only the emitted upstream failure, omitting values. Given a Publisher<Event<String, MyError>, Never>, this operator will return a Publisher<MyError, Never>.
Note: This operator only works on publishers that were materialized with the materialize() operator.
let values = PassthroughSubject<String, MyError>()
enum MyError: Swift.Error {
case ohNo
}
values
.materialize()
.failures()
.sink(receiveValue: { print("failures: \($0)") })
values.send("Hello")
values.send("World")
values.send("What's up?")
values.send(completion: .failure(.ohNo))
Output:
failure: MyError.ohNo
dematerialize
Converts a previously-materialized publisher into its original form. Given a Publisher<Event<String, MyError>, Never>, this operator will return a Publisher<String, MyError>
Note: This operator only works on publishers that were materialized with the materialize() operator.
partition
Partition a publisher's values into two separate publishers of values that match, and don't match, the provided predicate.
let source = PassthroughSubject<Int, Never>()
let (even, odd) = source.partition { $0 % 2 == 0 }
even.sink(receiveValue: { print("even: \($0)") })
odd.sink(receiveValue: { print("odd: \($0)") })
source.send(1)
source.send(2)
source.send(3)
source.send(4)
source.send(5)
Output:
odd: 1
even: 2
odd: 3
even: 4
odd: 5
ZipMany
This repo includes two overloads on Combine’s Publisher.zip methods (which, at the time of writing only go up to arity three).
This lets you arbitrarily zip many publishers and receive an array of inner publisher outputs back.
let first = PassthroughSubject<Int, Never>()
let second = PassthroughSubject<Int, Never>()
let third = PassthroughSubject<Int, Never>()
let fourth = PassthroughSubject<Int, Never>()
subscription = first
.zip(with: seco
