Combine | (II) Combine Operator:转换 |过滤 |组合

3,355 阅读8分钟

Combine | (II) Combine Operator:转换 |过滤 |组合

操作符(Operator)是 Combine 生态的重要组成部分,使我们以有意义的方式操作上游 Publisher 发出的值。我们将了解 Combine 的大多数 Operator:转换(Transforming)、过滤(Filtering)、组合(Combining)、时间操作(Time Manipulation)和序列(Sequence)。

转换操作符(Transforming Operators)

Combine 的每个 Operator 都返回一个 Publisher。 一般来说,Publisher 接收上游事件,对其进行某些操作,将操作后的事件向下游发送给消费者。

收集值

collect()

func collect() -> Publishers.Collect<Self>
func collect(_ count: Int) -> Publishers.CollectByCount<Self>

一个 Publisher 可以发出单个值或值的集合。collect() Operator 是将来自 Publisher 的单值流转换为单个数组的方法。以下弹珠图图描述了 collect() 缓冲单值流,直到上游的 Publisher 完成,然后它向下游发出该数组:

collect.png

新建 Playground 并添加以下代码:

import Combine
import Foundation

func example(_ desc: String, _ action:() -> Void) {
    print("--- \(desc) ---")
    action()
}

var subscriptions = Set<AnyCancellable>()

example("Collect") {
    ["A", "B", "C", "D", "E"]
        .publisher
        .sink { print($0) } 
            receiveValue: { print($0) }
        .store(in: &subscriptions)
}

运行 Playground,我们会看到每个值都出现在单独的行上,最后跟着一个完成事件:

--- Collect ---
A
B
C
D
E
finished

现在在调用 sink 之前使用 collect():

example("Collect") {
    ["A", "B", "C", "D", "E"]
        .publisher
        .collect()
        .sink { print($0) } 
            receiveValue: { print($0) }
        .store(in: &subscriptions)
}

再次运行 Playground,我们将看到接收到单个数组值,然后是完成事件:

--- Collect ---
["A", "B", "C", "D", "E"]
finished

但在使用 collect() 和其他不需要指定 count 这类 Operator 时要小心,,因为它们不会在上游完成之前发出值,所以它们可能将使用大量内存来存储接收到的值。

collect() Operator 有一些变体。例如我们可以指定收集一定数量的值,将上游切割成多个“批次”。我们可以将上述实例代码中的 .collect() 替换为:

.collect(2)

再次运行 Playground,我们将看到以下输出:

--- Collect ---
["A", "B"]
["C", "D"]
["E"]
finished

因为上游 Publisher 在 collect() 填满其缓冲区之前就完成了,所以它将剩余的内容作为一个数组发送,即值 E 也作为一个数组。

映射值

map(_:)

func map<T>(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map<Self, T>

我们通常希望以某种方式转换值。 map 的工作方式与 Swift 的标准 map 类似。在弹珠图中,我们将每个值乘以 2,此 Operator 在上游发布值后立即发布值:

map.png

将这个新示例添加到我们的 Playground:

example("map") { 
    let formatter = NumberFormatter()
    formatter.numberStyle = .spellOut
    [9, 99, 999, 9999].publisher
        .map { 
            formatter.string(for: NSNumber(integerLiteral: $0)) ?? ""
        }
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

在这里,我们创建一个 formatter 来拼出每个数字。创建 Int Publisher 后,使用 map,闭包中获取上游值并返回字符串。运行 Playground,我们将看到以下输出:

--- map ---
九
九十九
九百九十九
九千九百九十九

map Operator 的 keyPath 有三个,它们可以使用 keyPath 映射到值的一个、两个或三个属性。 他们的签名如下:

func map<T>(
  _ keyPath: KeyPath<Self.Output, T>
) -> Publishers.MapKeyPath<Self, T>

func map<T0, T1>(
    _ keyPath0: KeyPath<Self.Output, T0>,
    _ keyPath1: KeyPath<Self.Output, T1>
) -> Publishers.MapKeyPath2<Self, T0, T1>

func map<T0, T1, T2>(
    _ keyPath0: KeyPath<Self.Output, T0>,
    _ keyPath1: KeyPath<Self.Output, T1>,
    _ keyPath2: KeyPath<Self.Output, T2>
) -> Publishers.MapKeyPath3<Self, T0, T1, T2>

将以下示例添加到 Playground:

example("map & keyPath") { 
    struct Persion {
        var name: String
        var height: Float
        var weight: Float
    }
    let publisher = PassthroughSubject<Persion, Never>()
    publisher
        .map(\.name, \.height, \.weight)
        .map { "\($0)'s height is \($1) cm and weight is \($2) kg." }
        .sink { print($0) }
        .store(in: &subscriptions)
    publisher.send(.init(name: "A", height: 180, weight: 80))
    publisher.send(.init(name: "B", height: 170, weight: 60))
}

我们定义了 Persion 结构,并订阅 publisher,将相关字段转换 String 并输出。运行 Playground,输出将如下所示:

--- map & keyPath ---
A's height is 180.0 cm and weight is 80.0 kg.
B's height is 170.0 cm and weight is 60.0 kg.

tryMap(_:)

func mapError<E>(_ transform: @escaping (Self.Failure) -> E) -> Publishers.MapError<Self, E> where E : Error

包括 map 在内的几个 Operator 都有一个带有 try 前缀的方法,该前缀的方法接受一个可抛出错误的闭包。方法中抛出的错误,将在下游被接收:

example("tryMap") {
    Just("Path")
        .tryMap { try FileManager.default.contentsOfDirectory(atPath: $0) }
        .sink { print($0) } 
            receiveValue: { print($0) }
        .store(in: &subscriptions)
}

我们创建了一个 String Publisher,使用 tryMap 尝试获取文件内容,接收并打印出完成事件或值。运行 Playground,我们将看到错误事件:

--- tryMap ---
failure(Error Domain=NSCocoaErrorDomain Code=260 "文件夹“Path”不存在。" UserInfo={NSFilePath=Path, NSUserStringVariant=(
    Folder
), NSUnderlyingError=0x15aec8790 {Error Domain=NSOSStatusErrorDomain Code=-43 "fnfErr: File not found"}})

扁平化 Publisher

flatMap(maxPublishers:_:)

func flatMap<T, P>(
    maxPublishers: Subscribers.Demand = .unlimited,
    _ transform: @escaping (Self.Output) -> P
) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure

flatMap Operator 将多个上游 Publisher 展平为一个下游 Publisher。flatMap 返回的 Publisher 与接收的上游 Publisher 通常是不同的。在 Combine 中 flatMap 的一个常见用例,是当我们想要将一个 Publisher 发出的元素,传递给一个返回 Publisher 的方法,并最终订阅第二个 Publisher 发出的元素。在 Playground 中添加新代码:

example("flatMap") {    
    [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]
        .publisher
        .collect()
        .flatMap { codes in
            Just(codes.map { String(UnicodeScalar($0)) }.joined())
        }
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们将 ASCII 编码的 Int 数组转换为 publisher,将这些值 collect 为一个 Int 数组。使用 flatMap 转换为一个 Just Publisher,该 Publisher 将 Int 数组转变为一个 String 类型的值。接着订阅 Just Publisher,打印该值。最终输出如下:

--- flatMap ---
Hello, World!

上面的示例中,我们使用了方法的第二个入参,提供了一个 transform 闭包,还有一个 maxPublishers 参数。

flatMap 将所有接收到的 Publisher 的输出扁平化为一个 Publisher,因为它会缓冲多个 Publisher,来更新它在下游发出的单个 Publisher,这可能会引起内存问题。而 maxPublishers 为我们做了限制:

flatMap.png

在图中,flatMap 接收三个发布者:p0、p1 和 p2。 flatMap 从 p1 和 p2 发出值,但忽略 p3,因为 maxPublishers 设置为 2。上述逻辑用代码描述为:

example("flatMap & maxPublishers") {    
    let publisher = PassthroughSubject<AnyPublisher<String, Never>, Never>()
    publisher
        .flatMap(maxPublishers: .max(2)) { $0 }
        .sink { print($0) } 
            receiveValue: { print($0) }
        .store(in: &subscriptions)
    
    let p0 = CurrentValueSubject<String, Never>("p0 - 0")
    let p1 = CurrentValueSubject<String, Never>("p1 - 0")
    let p2 = CurrentValueSubject<String, Never>("p2 - 0")
    
    publisher.send(p0.eraseToAnyPublisher())
    publisher.send(p1.eraseToAnyPublisher())
    publisher.send(p2.eraseToAnyPublisher())
    
    p0.send("p0 - 1")
    p1.send("p1 - 1")
    p2.send("p2 - 1")
    p0.send("p0 - 2")
    p1.send("p1 - 2")
    p2.send("p2 - 2")
}

添加到 Playground 并运行,控制台将输出:

--- flatMap & maxPublishers ---
p0 - 0
p1 - 0
p0 - 1
p1 - 1
p0 - 2
p1 - 2

替换和插入值

replaceNil(with:)

func replaceNil<T>(with output: T) -> Publishers.Sequence<[Publishers.Sequence<Elements, Failure>.Output], Failure> where Elements.Element == T?

当我们希望始终提供非 Nil 值时可以使用该 Operator。如下图所示,replaceNil 接收可选值并将 nil 值替换为我们指定的值:

replaceNil.png

将以下代码添加到 Playground:

example("replaceNil") {
    [Optional(1), nil, Optional(3)].publisher
        .eraseToAnyPublisher()
        .replaceNil(with: 2)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们从可选 Int 数组创建 Publisher。使用 replaceNil(with:) 将来自上游发布者的 nil 值替换为 2:

--- replaceNil ---
1
2
3

上述代码使用 eraseToAnyPublisher() 调整了编译器的类型推断,否则 Optional<String> 不会被展开。

replaceEmpty(with:)

func replaceNil<T>(with output: T) -> Publishers.Map<Self, T> where Self.Output == T?

如果 Publisher 完成了但没有发出一个值,我们可以使用 replaceEmpty(with:) Operator 来插入一个值。在下面的弹珠图中,Publisher 完成时没有发出任何内容,此时 replaceEmpty(with:) 操作符插入一个值并将其发布到下游:

replaceEmpty.png

在 Playground 中添加这个新示例以查看它的实际效果:

example("replaceEmpty") {
    let empty = Empty<Int, Never>()
    empty
        .replaceEmpty(with: 1)
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们创使用 Empty Publisher 立即发出 .finished 事件。而 .replaceEmpty(with: 1) 将会在完成事件发生时,插入值 1:

--- replaceEmpty ---
1
finished

值的增量输出

scan(_:_:)

func scan<T>(
    _ initialResult: T,
    _ nextPartialResult: @escaping (T, Self.Output) -> T
) -> Publishers.Scan<Self, T>

scan 将上次闭包返回的值,和上游 Publisher 发出的最新值提供给闭包。在下面的弹珠图中,scan 从 0 开始,当它从 Publisher 接收每个值时,它将其加到先前存储的值中,然后发出结果:

scan.png

上述图用代码描述为:

example("scan") {
    [1, 2, 3]
        .publisher
        .scan(0) { latest, current in
            latest + current
        }
        .sink(receiveValue: { print($0)})
        .store(in: &subscriptions)
}

运行 Playground 将输出:

--- scan ---
1
3
6

过滤操作符(Filtering Operators)

当我们想要限制 Publisher 发出的值或事件,只使用其中需要的一部分时,可以使用使用一组特殊的 Operator 来做到这一点:过滤操作符。

过滤值

filter(_:)

func filter(_ isIncluded: @escaping (Self.Output) -> Bool) -> Publishers.Filter<Self>

我们将了解基础过滤——消费 Publisher 的值,并有条件地决定将其中的哪些传递给下游的消费者。filter 采用返回 Bool 的闭包,只传递与条件匹配的值:

filter.png

将这个新示例添加到 Playground:

example("filter") {
    (1...10)
        .publisher
        .filter { $0.isMultiple(of: 3) }
        .sink(receiveValue: { print("\($0) is a multiple of 3!")})
        .store(in: &subscriptions)
}

在上面的示例中,我们创建一个新的 Publisher,它将发出从 1 到 10。使用 filter,只允许通过是三的倍数的数字:

--- filter ---
3 is a multiple of 3!
6 is a multiple of 3!
9 is a multiple of 3!

removeDuplicates()

func removeDuplicates() -> Publishers.RemoveDuplicates<Self>

很多时候我们可能会遇到 Publisher 发出相同的值,我们可能希望忽略这些值:Combine 为该任务提供了 Operator:

removeDuplicates.png

将以下示例添加到我们的 Playground:

example("removeDuplicates") {
    ["a", "b", "b", "b", "c", "d", "d", "e", "f", "f"]
        .publisher
        .removeDuplicates()
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们过滤了数组中重复的字符串,运行我们的 Playground 并查看控制台:

--- removeDuplicates ---
a
b
c
d
e
f

移除 nil

compactMap(_:)

func compactMap<T>(_ transform: @escaping (Self.Output) -> T?) -> Publishers.CompactMap<Self, T>

Swift 标准库中的一个非常著名的方法:compactMap—— Combine 有一个同名的 Operator:

compactMap.png

将以下内容添加到我们的 Playground:

example("compactMap") {
    ["a", "1", "2", "b", "3", "4", "c"]
        .publisher
        .compactMap { Int($0) }
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们创建一个 String Publisher,使用 compactMap 尝试从每个单独的字符串初始化一个 Int。 如果失败,则返回 nil。 这些 nil 值会被 compactMap 过滤。在我们的 Playground 中运行上面的示例:

--- compactMap ---
1
2
3
4

忽略值

ignoreOutput()

func ignoreOutput() -> Publishers.IgnoreOutput<Self>

有时我们只关心 Publisher 完成事件的发送,而忽略值。我们可以使用 ignoreOutput

ignoreOutput.png

将以下代码添加到我们的 Playground:

example("ignoreOutput") {
    (1...100)
        .publisher
        .ignoreOutput()
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们创建一个 Publisher,从 1 到 100 发出 100 个值,添加 ignoreOutput() Operator,它会忽略所有值,只向消费者发出完成事件:

--- ignoreOutput ---
finished

寻找值

first() first(where:)

func first() -> Publishers.First<Self>
func first(where predicate: @escaping (Self.Output) -> Bool) -> Publishers.FirstWhere<Self>

last() last(where:)

func last() -> Publishers.Last<Self>
func last(where predicate: @escaping (Self.Output) -> Bool) -> Publishers.LastWhere<Self>

同样起源于 Swift 标准库的 Operator:first(where:)last(where:)。 我们可以使用它们分别仅查找和发出与提供闭包条件匹配的第一个或最后一个值:

firstAndLast.png

将以下代码添加到我们的 Playground:

example("first(where:)") {
     (1...5)
        .publisher
        .first(where: { $0 % 2 == 0 })
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}

example("last(where:)") {
     (1...5)
        .publisher
        .last(where: { $0 % 2 == 0 })
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们分别查找了发出的第一个和最后一个偶数值:

--- first(where:) ---
2
finished
--- last(where:) ---
4
finished

一旦 first(where:) 找到匹配的值,它就会通过 Subscription 发送取消,上游停止发出值。可以添加 print() 验证:

example("first(where:)") {
     (1...5)
        .publisher
        .print()
        .first(where: { $0 % 2 == 0 })
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}

控制台将输出:

--- first(where:) ---
receive subscription: (1...5)
request unlimited
receive value: (1)
receive value: (2)
receive cancel
2
finished

first(where:) 是 lazy的,而 last(where:) 则是贪婪的,需要等 Publisher 发送完成事件后,才能确定所需的值。

删除值

dropFirst(_:)

func dropFirst(_ count: Int = 1) -> Publishers.Drop<Self>

dropFirst Operator 需要 count 参数(如果省略,则默认为 1),忽略 Publisher 发出的前 count 个值:

dropFirst.png

将以下代码添加到 Playground:

example("dropFirst") {
    (1...5)
        .publisher
        .dropFirst(3)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们创建一个 Publisher 发出 1 到 5 之间的数字。使用 dropFirst(3) 删除前三个值:

--- dropFirst ---
4
5

drop(while:)

func drop(while predicate: @escaping (Self.Output) -> Bool) -> Publishers.DropWhile<Self>

drop(while:) 是一个非常有用的变体,它用条件闭包忽略 Publisher 发出的任何值,直到第一次满足该条件后,值就可以通过:

dropWhile.png

将以下代码添加到 Playground:

example("drop(while:)") {
    let numbers = (1...5)
        .publisher
        .drop(while: { $0 % 4 != 0 })
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

我们创建了一个发出 1 到 5 之间数字的 Publisher,使用 drop(while:) 等待可以被 4 整除的第一个值:

--- drop(while:) ---
4
5

drop(while:) 的闭包在满足条件后将不再执行,修改上述代码:

example("drop(while:)") {
    let numbers = (1...5)
        .publisher
        .drop(while: {
            print("Checking \($0)")
            return $0 % 4 != 0
        })
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

控制台将输出:

--- drop(while:) ---
Checking 1
Checking 2
Checking 3
Checking 4
4
5

drop(untilOutputFrom:)

func drop<P>(untilOutputFrom publisher: P) -> Publishers.DropUntilOutput<Self, P> where P : Publisher, Self.Failure == P.Failure

drop(untilOutputFrom:) 会跳过 Publisher 发出的任何值,直到作为参数的 Publisher 开始发出值:

dropUntilOutputFrom.png

我们用代码来描述这个过程:

example("drop(untilOutputFrom:)") {
    let isReady = PassthroughSubject<Void, Never>()
    let publisher = PassthroughSubject<Int, Never>()
    publisher
        .drop(untilOutputFrom: isReady)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    (1...5).forEach { num in
        publisher.send(num)
        if num == 3 {
            isReady.send()
        }
    }
}

我们创建了两个 PassthroughSubject。使用 drop(untilOutputFrom: isReady) 忽略 publisher 的值,直到 isReady 发出至少一个值,在 publisher 发出 3 后, isReady 发出值:

--- drop(untilOutputFrom:) ---
4
5

限制值

prefix(_:)

func prefix(_ maxLength: Int) -> Publishers.Output<Self>

prefix(while:)

func prefix(while predicate: @escaping (Self.Output) -> Bool) -> Publishers.PrefixWhile<Self>

prefix(untilOutputFrom:)

func prefix<P>(untilOutputFrom publisher: P) -> Publishers.PrefixUntilOutput<Self, P> where P : Publisher

与删除值的一系列 Operator 相反,prefix 系列将只取所提供的数量的值,prefix 系列是 lazy 的,获取所需的值后即发送完成事件:

prefix.png

将以下代码添加到 Playground:

example("prefix") {
    (1...5)
        .publisher
        .prefix(2)
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}

example("prefix(while:)") {
    (1...5)
        .publisher
        .prefix(while: { $0 < 3 })
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}

example("prefix(untilOutputFrom:)") {
    let isReady = PassthroughSubject<Void, Never>()
    let publisher = PassthroughSubject<Int, Never>()
    
    publisher
        .prefix(untilOutputFrom: isReady)
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
    (1...5).forEach { num in
        publisher.send(num)
        if num == 2 {
            isReady.send()
        }
    }
}

运行 Playground,控制台将输出:

--- prefix ---
1
2
finished
--- prefix(while:) ---
1
2
finished
--- prefix(untilOutputFrom:) ---
1
2
finished

组合操作符(Combining Operators)

这组 Operator 允许我们组合不同 Publisher 发出的值或事件,在代码中创建有意义的数据组合。

前置值

prepend(_:)

func prepend(_ elements: Self.Output...) -> Publishers.Concatenate<Publishers.Sequence<[Self.Output], Self.Failure>, Self>

... 语法指可变参数列表,这意味着该方法入参可以为任意数量的与原始 Publisher 的 Output 类型相同的值:

prepend.png

将以下代码添加到我们的 Playground:

example("prepend") {
    [4, 5]
        .publisher
        .prepend(1, 2, 3)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

在上面的代码中,我们创建一个发布数字 4、5 的 Publisher,使用 prepend 在 Publisher 的值之前添加数字 1 、2、3。运行 Playground,我们会在调试控制台中看到以下内容:

--- prepend ---
1
2
3
4
5

我们可以轻松添加多个前置:

example("prepend") {
    [4, 5]
        .publisher
        .prepend(1, 2, 3)
        .prepend(-1, 0)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

将会输出:

--- prepend ---
-1
0
1
2
3
4
5

prepend(Sequence)

func prepend<S>(_ elements: S) -> Publishers.Concatenate<Publishers.Sequence<S, Self.Failure>, Self> where S : Sequence, Self.Output == S.Element

prepend 的这种变体将任何符合 Sequence 的对象作为输入。 例如 Array 或 Set:

将以下代码添加到你的 Playground:

example("prepend(Sequence)") {
    [7, 8].publisher
        .prepend([5, 6])
        .prepend(Set(3...4))
        .prepend(stride(from: 0, to: 3, by: 1))
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

在此代码中,我们创建了一个发布数字 7、8 的 Publisher。三次链接 prepend(Sequence) 到 Publisher, 一次从 Array 中添加值,第二次从 Set 中添加值、第三次从符合 SequenceStrideable 中添加值:

--- prepend(Sequence) ---
0
1
2
3
4
5
6
7
8

Set 是无序的,意味着上例中的两个值可以是 3 和 4,也可以是 4 和 5。

prepend(Publisher)

func prepend<P>(_ publisher: P) -> Publishers.Concatenate<P, Self> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output

前两个 Operator 将值列表添加到现有 Publisher。 如果我们有两个不同的 Publisher,并且想将它们的值合在一起,我们可以使用 prepend(Publisher) 在 Publisher 的值之前添加第二个 Publisher 发出的值:

example("prepend(Publisher)") {
    let publisher1 = [3, 4].publisher
    let publisher2 = [1, 2].publisher
    publisher1
        .prepend(publisher2)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

运行 Playground,控制台将输出:

--- prepend(Publisher) ---
1
2
3
4

该 Operator 还有一些细节,我们继续添加以下内容添加到 Playground:

example("prepend(Publisher) V2") {
    let publisher1 = [3, 4].publisher
    let publisher2 = PassthroughSubject<Int, Never>()
    publisher1
        .prepend(publisher2)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    publisher2.send(1)
    publisher2.send(2)
}

我们创建了两个 Publisher。 第一个发出值 3 和 4,而第二个是动态接受值的 PassthroughSubject。在 publisher1 的值之前添加 publisher2 的值,接着 publisher2 发送值 1 和 2。运行 Playground:

--- prepend(Publisher) V2 ---
1
2

为什么 publisher2 只发出两个值? 因为 publisher2 发出了值,但没有发出完成事件,被前置的 Publisher 必须发出完成事件后,Combine 才知道需要进行切换 Publisher:

example("prepend(Publisher) V2") {
    let publisher1 = [3, 4].publisher
    let publisher2 = PassthroughSubject<Int, Never>()
    publisher1
        .prepend(publisher2)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    publisher2.send(1)
    publisher2.send(2)
    publisher2.send(completion: .finished)
}

现在,控制台才会输出:

--- prepend(Publisher) V2 ---
1
2
3
4

追加值

append(_:)

func append(_ elements: Self.Output...) -> Publishers.Concatenate<Self, Publishers.Sequence<[Self.Output], Self.Failure>>

append 的工作方式与 prepend 类似:它也接受一个 Output 类型的可变参数列表,然后在原始 Publisher 完成事件后,附加在最终的完成事件前:

append.png

将以下代码添加到 Playground:

example("append") {
    [1, 2, 3]
        .publisher
        .append(4, 5)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

在上面的代码中,创建一个发布 1、2、3 的 Publisher,使用 append,追加 4 和 5:

--- append ---
1
2
3
4
5

append 的工作方式与我们期望的完全一样,等待上游完成事件后,然后再添加自己的值。这意味着上游必须发出完成事件,否则 append 永远不会发生。

append(Sequence)

func append<S>(_ elements: S) -> Publishers.Concatenate<Self, Publishers.Sequence<S, Self.Failure>> where S : Sequence, Self.Output == S.Element

prepend 类似,append 的变体可以追加符合 Sequence 的对象:

example("append(Sequence)") {
    [1, 2, 3]
        .publisher
        .append([4, 5])
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

运行 Playground 将输出:

--- append(Sequence) ---
1
2
3
4
5

append(Publisher)

func append<P>(_ publisher: P) -> Publishers.Concatenate<Self, P> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output

prepend 类似,append 的变体可以追加另一个 Publisher:

example("append(Publisher)") {
    let publisher1 = [1, 2].publisher
    let publisher2 = [3, 4].publisher
    publisher1
        .append(publisher2)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

运行 Playground 将输出:

--- append(Publisher) ---
1
2
3
4

同样需要注意,只有前一个 Publisher 发布完成事件,才能追加另一个 Publisher。

其他高级组合操作符

switchToLatest()

func switchToLatest() -> Publishers.SwitchToLatest<Self.Output, Self>

switchToLatest() 使我们可以在取消对旧 Publisher 订阅的同时,切换到对新 Publisher 的订阅:

switchToLatest.png

将以下代码添加到我们的 Playground,描述上述弹珠图:

example("switchToLatest") {
    let publishers = PassthroughSubject<PassthroughSubject<Int, Never>, Never>()
    let numbers1 = PassthroughSubject<Int, Never>()
    let numbers2 = PassthroughSubject<Int, Never>()
    let numbers3 = PassthroughSubject<Int, Never>()
    
    publishers
        .switchToLatest()
        .sink(
            receiveCompletion: { print($0) },
            receiveValue: { print($0) }
        )
        .store(in: &subscriptions)
    
    publishers.send(numbers1)
    numbers1.send(1)
    numbers1.send(2)
    
    publishers.send(numbers2)
    numbers1.send(3)
    numbers1.send(completion: .finished)
    numbers2.send(4)
    numbers2.send(5)
    
    publishers.send(numbers3)
    numbers2.send(6)
    numbers2.send(completion: .finished)
    numbers3.send(7)
    numbers3.send(8)
    numbers3.send(9)
    numbers3.send(completion: .finished)
    
    publishers.send(completion: .finished)
}

我们创建一个 PassthroughSubject 发出其他 PassthroughSubject 值,接着创建三个 PassthroughSubject。在 publishers 使用 switchToLatest。现在,每次我们通过 publishers 发送 numbers1 numbers2 numbers3 时,我们都会切换到新的 Publisher 并取消之前的 Subscription。最终,我们的控制台将打印:

--- switchToLatest ---
1
2
4
5
7
8
9
finished

在实际应用中,例如用户点击触发网络请求的按钮,再未拿到请求结果时,用户再次点击按钮,触发第二个网络请求。此时,可以使用 switchToLatest() 来只使用新的请求结果。

merge(with:)

func merge(with other: Self) -> Publishers.MergeMany<Self>

此 Operator 将合并来自同一类型的不同 Publisher 的值,如下所示:

merge.png

将以下代码添加到我们的 Playground:

example("merge(with:)") {
    let publisher1 = PassthroughSubject<Int, Never>()
    let publisher2 = PassthroughSubject<Int, Never>()
    
    publisher1
        .merge(with: publisher2)
        .sink(receiveCompletion: { print($0) },
            receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    publisher1.send(1)
    publisher1.send(2)
    publisher2.send(3)
    publisher1.send(4)
    publisher1.send(completion: .finished)
    publisher2.send(5)
    publisher2.send(completion: .finished)
}

在上述代码中,我们创建来了两个 PassthroughSubjects 。将 publisher1publisher2 mergemerge 提供了重载,可让我们合并多达八个不同的 Publisher。将 1 和 2 添加到 publisher1,然后将 3 添加到 publisher2,然后再次将 4 添加到 publisher1 并发送完成事件,最后将 5 添加到 publisher2 并发送完成事件。

运行我们的 Playground:

--- merge(with:) ---
1
2
3
4
5
finished

merge 将会等待所有 Publisher 发出完成事件后才会发出最终的完成事件。

combineLatest(_:_:)

func combineLatest<P, T>(
    _ other: P,
    _ transform: @escaping (Self.Output, P.Output) -> T
) -> Publishers.Map<Publishers.CombineLatest<Self, P>, T> where P : Publisher, Self.Failure == P.Failure

combineLatest 是另一个允许我们组合不同 Publisher 的操作符。 它还允许我们组合不同值类型的Publisher。它在所有 Publisher 发出值时,发出一个包含所有 Publisher 的最新值的元组。

所以会有一个问题:Publisher 和传递给 combineLatest 的每个 Publisher 都必须至少发出一个值,然后 combineLatest 本身才会发出值:

combineLatest.png

将以下代码添加到我们的 Playground:

example("combineLatest") {
    let publisher1 = PassthroughSubject<Int, Never>()
    let publisher2 = PassthroughSubject<String, Never>()
    
    publisher1
        .combineLatest(publisher2)
        .sink(
            receiveCompletion: { print($0) },
            receiveValue: { print("P1: \($0), P2: \($1)") }
        )
        .store(in: &subscriptions)
    
    publisher1.send(1)
    publisher1.send(2)
    
    publisher2.send("a")
    publisher2.send("b")
    
    publisher1.send(3)
        publisher1.send(completion: .finished)
    
    publisher2.send("c")
    publisher2.send(completion: .finished)
}

我们创建两个 PassthroughSubjects。将 publisher2 的与 publisher1 combineLatest 起来。 我们可以使用不同的 combineLatest 的重载组合最多四个不同的 Publisher。将 1 和 2 发送到 publisher1,然后将“a”和“b”发送到 publisher2,然后将 3 发送到 publisher1 并发送完成事件,最后将“c”发送到 publisher2 并发送完成事件。

最终,控制台将输出:

--- combineLatest ---
P1: 2, P2: a
P1: 2, P2: b
P1: 3, P2: b
P1: 3, P2: c
finished

zip(_:)

func zip<P>(_ other: P) -> Publishers.Zip<Self, P> where P : Publisher, Self.Failure == P.Failure

该 Operator 的工作方式与标准库类似,发出不同 Publisher 的相同索引的成对值的元组。即它等待每个 Publisher 发出一个值,然后在所有 Publisher 在当前索引处发出一个值后,发出一个元组:

zip.png

将以下代码添加到我们的 Playground:

example("zip") {
    let publisher1 = PassthroughSubject<Int, Never>()
    let publisher2 = PassthroughSubject<String, Never>()
    
    publisher1
        .zip(publisher2)
        .sink(receiveCompletion: { print($0) },
            receiveValue: { print("P1: \($0), P2: \($1)") })
        .store(in: &subscriptions)
    
    publisher1.send(1)
    publisher1.send(2)
    
    publisher2.send("a")
    publisher2.send("b")
    
    publisher1.send(3)
    publisher1.send(completion: .finished)
    
    publisher2.send("c")
    publisher2.send("d")
    publisher2.send(completion: .finished)
}

我们创建两个 PassthroughSubject。将 publisher1 与 publisher2 zip。将 1 和 2 发送到 publisher1,然后将“a”和“b”发送到 publisher2,然后将 3 再次发送到 publisher1 并发送完成事件,最后将“c”和“d”发送到 publisher2 并发送完成事件。

最后一次运行我们的 Playground 并查看调试控制台:

--- zip ---
P1: 1, P2: a
P1: 2, P2: b
P1: 3, P2: c
finished

内容参考