Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/combinecommunity/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.
https://github.com/combinecommunity/combineext

combine-framework reactive-programming reactive-streams swift

Last synced: 30 days ago
JSON representation

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.

Awesome Lists containing this project

README

        

# CombineExt






Build Status
Code Coverage for CombineExt on codecov





CombineExt supports CocoaPods
CombineExt supports Swift Package Manager (SPM)
CombineExt supports Carthage

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](#withLatestFrom)
* [flatMapLatest](#flatMapLatest)
* [assign](#assign)
* [amb and Collection.amb](#amb)
* [materialize](#materialize)
* [values](#values)
* [failures](#failures)
* [dematerialize](#dematerialize)
* [partition](#partition)
* [zip(with:) and Collection.zip](#ZipMany)
* [Collection.merge()](#MergeMany)
* [combineLatest(with:) and Collection.combineLatest](#CombineLatestMany)
* [mapMany(_:)](#MapMany)
* [filterMany(_:)](#FilterMany)
* [setOutputType(to:)](#setOutputType)
* [removeAllDuplicates and removeAllDuplicates(by:) ](#removeAllDuplicates)
* [share(replay:)](#sharereplay)
* [prefix(duration:tolerance:​on:options:)](#prefixduration)
* [prefix(while:behavior:​)](#prefixwhilebehavior)
* [toggle()](#toggle)
* [nwise(_:) and pairwise()](#nwise)
* [ignoreOutput(setOutputType:)](#ignoreOutputsetOutputType)
* [ignoreFailure](#ignoreFailure)
* [mapToResult](#mapToResult)
* [flatMapBatches(of:)](#flatMapBatchesof)

### Publishers
* [AnyPublisher.create](#AnypublisherCreate)
* [CurrentValueRelay](#CurrentValueRelay)
* [PassthroughRelay](#PassthroughRelay)

### Subjects
* [ReplaySubject](#ReplaySubject)

> **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

### CocoaPods

Add the following line to your **Podfile**:

```rb
pod 'CombineExt'
```

### Swift Package Manager

Add the following dependency to your **Package.swift** file:

```swift
.package(url: "https://github.com/CombineCommunity/CombineExt.git", from: "1.0.0")
```

### Carthage

Carthage support is offered as a prebuilt binary.

Add the following to your **Cartfile**:

```
github "CombineCommunity/CombineExt"
```

## 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.

```swift
let taps = PassthroughSubject()
let values = CurrentValueSubject("Hello")

taps
.withLatestFrom(values)
.sink(receiveValue: { print("withLatestFrom: \($0)") })

taps.send()
taps.send()
values.send("World!")
taps.send()
```

#### Output:

```none
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`.

```swift
let trigger = PassthroughSubject()
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.

```swift
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`.

```swift
// 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](http://reactivex.io/documentation/operators/amb.html), also known in RxJS as `race`.

```swift
let subject1 = PassthroughSubject()
let subject2 = PassthroughSubject()

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:

```none
amb: 3
amb: 6
amb: completed with .finished
```

------

### materialize

Convert any publisher to a publisher of its events. Given a `Publisher`, this operator will return a `Publisher, Never>`, which means your failure will actually be a regular value, which makes error handling much simpler in many use cases.

```swift
let values = PassthroughSubject()
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:

```none
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, Never>`, this operator will return a `Publisher`.

**Note**: This operator only works on publishers that were materialized with the `materialize()` operator.

```swift
let values = PassthroughSubject()
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:

```none
values: "Hello"
values: "World"
values: "What's up?"
```

------

### failures

Given a materialized publisher, publish only the emitted upstream failure, omitting values. Given a `Publisher, Never>`, this operator will return a `Publisher`.

**Note**: This operator only works on publishers that were materialized with the `materialize()` operator.

```swift
let values = PassthroughSubject()
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:

```none
failure: MyError.ohNo
```

------

### dematerialize

Converts a previously-materialized publisher into its original form. Given a `Publisher, Never>`, this operator will return a `Publisher`

**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.

```swift
let source = PassthroughSubject()

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:

```none
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.

```swift
let first = PassthroughSubject()
let second = PassthroughSubject()
let third = PassthroughSubject()
let fourth = PassthroughSubject()

subscription = first
.zip(with: second, third, fourth)
.map { $0.reduce(0, +) }
.sink(receiveValue: { print("zipped: \($0)") })

first.send(1)
second.send(2)
third.send(3)
fourth.send(4)
```

You may also use `.zip()` directly on a collection of publishers with the same output and failure types, e.g.

```swift
[first, second, third, fourth]
.zip()
.map { $0.reduce(0, +) }
.sink(receiveValue: { print("zipped: \($0)") })
```

#### Output:

```none
zipped: 10
```

------

### MergeMany

This repo includes an extension for Collection that allows you to call `.merge()` directly on a collection of publishers with the same output and failure types.

This lets you arbitrarily merge many publishers and receive inner publisher outputs back from a single publisher.

```swift
let first = PassthroughSubject()
let second = PassthroughSubject()
let third = PassthroughSubject()
let fourth = PassthroughSubject()

subscription = [first, second, third, fourth]
.merge()
.sink(receiveValue: { print("output: \($0)") })

first.send(1)
second.send(2)
third.send(3)
fourth.send(4)
```

#### Output:

```none
output: 1
output: 2
output: 3
output: 4
```

------

### CombineLatestMany

This repo includes two overloads on Combine’s `Publisher.combineLatest` methods (which, at the time of writing only go up to arity three) and an `Collection.combineLatest` constrained extension.

This lets you arbitrarily combine many publishers and receive an array of inner publisher outputs back.

```swift
let first = PassthroughSubject()
let second = PassthroughSubject()
let third = PassthroughSubject()
let fourth = PassthroughSubject()

subscription = [first, second, third, fourth]
.combineLatest()
.sink(receiveValue: { print("combineLatest: \($0)") })

first.send(true)
second.send(true)
third.send(true)
fourth.send(true)

first.send(false)
```

#### Output:

```none
combineLatest: [true, true, true, true]
combineLatest: [false, true, true, true]
```

------

### FilterMany
Filters element of a publisher collection into a new publisher collection.
```swift
let intArrayPublisher = PassthroughSubject<[Int], Never>()

intArrayPublisher
.filterMany { $0.isMultiple(of: 2) }
.sink(receiveValue: { print($0) })

intArrayPublisher.send([10, 2, 4, 3, 8])
```

#### Output:

```
none
[10, 2, 4, 8]
```

------

### MapMany

Projects each element of a publisher collection into a new publisher collection form.

```swift
let intArrayPublisher = PassthroughSubject<[Int], Never>()

intArrayPublisher
.mapMany(String.init)
.sink(receiveValue: { print($0) })

intArrayPublisher.send([10, 2, 2, 4, 3, 8])
```

#### Output:

```none
["10", "2", "2", "4", "3", "8"]
```

------

### setOutputType

`Publisher.setOutputType(to:)` is an analog to [`.setFailureType(to:)`](https://developer.apple.com/documentation/combine/publisher/3204753-setfailuretype) for when `Output` is constrained to `Never`. This is especially helpful when chaining operators after an [`.ignoreOutput()`](https://developer.apple.com/documentation/combine/publisher/3204714-ignoreoutput) call.

------

### removeAllDuplicates

`Publisher.removeAllDuplicates` and `.removeAllDuplicates(by:)` are stricter forms of Apple’s [`Publisher.removeDuplicates`](https://developer.apple.com/documentation/combine/publisher/3204745-removeduplicates) and [`.removeDuplicates(by:)`](https://developer.apple.com/documentation/combine/publisher/3204746-removeduplicates)—the operators de-duplicate across _all_ previous value events, instead of pairwise.

If your `Output` doesn‘t conform to `Hashable` or `Equatable`, you may instead use the comparator-based version of this operator to decide whether two elements are equal.

```swift
subscription = [1, 1, 2, 1, 3, 3, 4].publisher
.removeAllDuplicates()
.sink(receiveValue: { print("removeAllDuplicates: \($0)") })
```

#### Output:

```none
removeAllDuplicates: 1
removeAllDuplicates: 2
removeAllDuplicates: 3
removeAllDuplicates: 4
```

------

### share(replay:)

Similar to [`Publisher.share`](https://developer.apple.com/documentation/combine/publisher/3204754-share), `.share(replay:)` can be used to create a publisher instance with reference semantics which replays a pre-defined amount of value events to further subscribers.

```swift
let subject = PassthroughSubject()

let replayedPublisher = subject
.share(replay: 3)

subscription1 = replayedPublisher
.sink(receiveValue: { print("first subscriber: \($0)") })

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4)

subscription2 = replayedPublisher
.sink(receiveValue: { print("second subscriber: \($0)") })
```

#### Output:

```none
first subscriber: 1
first subscriber: 2
first subscriber: 3
first subscriber: 4
second subscriber: 2
second subscriber: 3
second subscriber: 4
```

### prefix(duration:)

An overload on `Publisher.prefix` that republishes values for a provided `duration` (in seconds), and then completes.

```swift
let subject = PassthroughSubject()

subscription = subject
.prefix(duration: 0.5, on: DispatchQueue.main)
.sink(receiveValue: { print($0) })

subject.send(1)
subject.send(2)
subject.send(3)

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
subject.send(4)
}
```

#### Output:

```none
1
2
3
```

### prefix(while:behavior:)

An overload on `Publisher.prefix(while:)` that allows for inclusion of the first element that doesn’t pass the `while` predicate.

```swift
let subject = PassthroughSubject()

subscription = subject
.prefix(
while: { $0 % 2 == 0 },
behavior: .inclusive
)
.sink(
receivecompletion: { print($0) },
receiveValue: { print($0) }
)

subject.send(0)
subject.send(2)
subject.send(4)
subject.send(5)
```

```none
0
2
4
5
finished
```

### toggle()

Toggle each boolean element of a publisher collection.

```swift
let subject = PassthroughSubject()

subscription = subject
.toggle()
.sink(receiveValue: { print($0) })

subject.send(true)
subject.send(false)
subject.send(true)
```

#### Output:

```none
false
true
false
```

### nwise

Groups the elements of the source publisher into arrays of N consecutive elements.

```swift
let subject = PassthroughSubject()

subscription = subject
.nwise(3)
.sink(receiveValue: { print($0) })

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4)
subject.send(5)
```

#### Output:

```none
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
```

### pairwise

Groups the elements of the source publisher into tuples of the previous and current elements

```swift
let subject = PassthroughSubject()

subscription = subject
.pairwise()
.sink(receiveValue: { print("\($0.0) -> \($0.1)") })

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4)
subject.send(5)
```

#### Output:

```none
1 -> 2
2 -> 3
3 -> 4
4 -> 5
```

### ignoreOutput(setOutputType:)

Shorthand for both ignoring a publisher’s value events and re-writing its `Output` generic.

```swift
let onlyAFour = ["1", "2", "3"].publisher
.ignoreOutput(setOutputType: Int.self)
.append(4)
```

### ignoreFailure

CombineExt provides a couple of overloads to ignore errors and optionally specify a new error type and whether to trigger completions in such cases.

- `ignoreFailure(completeImmediately:)`
- `ignoreFailure(setFailureType:completeImmediately:)`

```swift
enum AnError {
case someError
}

let subject = PassthroughSubject()

subscription = subject
.ignoreFailure() // The `completeImmediately` parameter defaults to `true`.
.sink(receiveValue: { print($0) }, receiveCompletion: { print($0) })

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(completion: .failure(.someError))
```

#### Output:

```none
1
2
3
.finished
```
------

### mapToResult

Transforms a publisher of type `AnyPublisher` to `AnyPublisher, Never>`

```swift
enum AnError: Error {
case someError
}

let subject = PassthroughSubject()

let subscription = subject
.mapToResult()
.sink(receiveCompletion: { print("completion: \($0)") },
receiveValue: { print("value: \($0)") })

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(completion: .failure(.someError))
```

#### Output

```none
value: success(1)
value: success(2)
value: success(3)
value: failure(AnError.someError)
completion: finished
```

------

### flatMapBatches(of:)

`Collection.flatMapBatches(of:)` subscribes to the receiver’s contained publishers in batches and returns their outputs in batches, too (while maintaining order). Subsequent batches of publishers are only subscribed to when prior batches successfully complete — any one failure is forwarded downstream.

```swift
let ints = (1...6).map(Just.init)

subscription = ints
.flatMapBatches(of: 2)
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
```

#### Output:

```none
[1, 2]
[3, 4]
[5, 6]
.finished
```

## Publishers

This section outlines some of the custom Combine publishers CombineExt provides

### AnyPublisher.create

A publisher which accepts a closure with a subscriber argument, to which you can dynamically send value or completion events.

This lets you easily create custom publishers to wrap any non-publisher asynchronous work, while still respecting the downstream consumer's backpressure demand.

You should return a `Cancellable`-conforming object from the closure in which you can define any cleanup actions to execute when the pubilsher completes or the subscription to the publisher is canceled.

```swift
AnyPublisher.create { subscriber in
// Values
subscriber.send("Hello")
subscriber.send("World!")

// Complete with error
subscriber.send(completion: .failure(MyError.someError))

// Or, complete successfully
subscriber.send(completion: .finished)

return AnyCancellable {
// Perform cleanup
}
}
```

You can also use an `AnyPublisher` initializer with the same signature:

```swift
AnyPublisher { subscriber in
/// ...
return AnyCancellable { }
```

------

### CurrentValueRelay

A `CurrentValueRelay` is identical to a `CurrentValueSubject` with two main differences:

* It only accepts values, but not completion events, which means it cannot fail.
* It only publishes a `.finished` event upon deallocation.

```swift
let relay = CurrentValueRelay("well...")

relay.sink(receiveValue: { print($0) }) // replays current value, e.g. "well..."

relay.accept("values")
relay.accept("only")
relay.accept("provide")
relay.accept("great")
relay.accept("guarantees")
```

#### Output:

```none
well...
values
only
provide
great
guarantees
```

------

### PassthroughRelay

A `PassthroughRelay` is identical to a `PassthroughSubject` with two main differences:

* It only accepts values, but not completion events, which means it cannot fail.
* It only publishes a `.finished` event upon deallocation.

```swift
let relay = PassthroughRelay()
relay.accept("well...")

relay.sink(receiveValue: { print($0) }) // does not replay past value(s)

relay.accept("values")
relay.accept("only")
relay.accept("provide")
relay.accept("great")
relay.accept("guarantees")
```

#### Output:

```none
values
only
provide
great
guarantees
```

## Subjects

### ReplaySubject

A Combine analog to Rx’s [`ReplaySubject` type](http://reactivex.io/documentation/subject.html). It’s similar to a [`CurrentValueSubject`](https://developer.apple.com/documentation/combine/currentvaluesubject) in that it buffers values, but, it takes it a step further in allowing consumers to specify the number of values to buffer and replay to future subscribers. Also, it will handle forwarding any completion events after the buffer is cleared upon subscription.

```swift
let subject = ReplaySubject(bufferSize: 3)

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4)

subject
.sink(receiveValue: { print($0) })

subject.send(5)
```

#### Output:

```none
2
3
4
5
```

## License

MIT, of course ;-) See the [LICENSE](LICENSE) file.

The Apple logo and the Combine framework are property of Apple Inc.