Combine Publisher详解

摘自《SwiftUI和Combine编程》---《Publisher 和常见 Operator》

Publisher 详解

Publisher 在接收到订阅,并且接受到请求要求提供若干个事件后,才开始对外发布事件。接受订阅和接受请求,也是 Publisher 生命周期的一部分。

Empty

Empty()

是一个最简单的 Publisher,它只在被订阅的时候发布一个完成事件 (receive finished)。这个 publisher 不会输出任何的 output 值,只能用于表示某个事件已经发生。

/// A publisher that never publishes any values, and optionally finishes immediately.
///
/// You can create a ”Never” publisher — one which never sends values and never finishes or fails — with the initializer `Empty(completeImmediately: false)`.
public struct Empty : Publisher, Equatable where Failure : Error

Just

Just(1)

表示一个单一的值,在被订阅后,这个值会被发送出去,紧接着是 finished

/// A publisher that emits an output to each subscriber just once, and then finishes.
///
/// You can use a ``Just`` publisher to start a chain of publishers. A ``Just`` publisher is also useful when replacing a value with ``Publishers/Catch``.
public struct Just : Publisher

Publisher.Sequence

如果我们对一连串的值感兴趣的话,可以使用的是 Publishers.Sequence。顾名思义,Publishers.Sequence 接受一个 Sequence:它可以是一个数组,也可以是一个 Range。在被订阅时,Sequence 中的元素被逐个发送出来

Publisher.Sequence<[Int], Never>(sequence: [1,2,3])
//等价于
[1,2,3].publisher

常见 Operator

map

元素变形:map 操作可以为我们完成事件中数据类型的转换

[1,2,3].publisher.map { $0*2 }

compactMap

将 map 结果中那些 nil 的元素去除掉,这个操作通常会“压缩”结果,让其中的元素数减少,这也正是其名字中 compact 的来源

flatMap

展平 降维

map 及 compactMap 的闭包返回值是单个的 Output 值。而与它们不同,flatMap 的变形闭包里需要返回一个 Publisher。也就是说,flatMap 将会涉及两个 Publisher:一个是 flatMap 操作本身所作用的外层 Publisher,一个是 flatMap 所接受的变形闭包中返回的内层 Publisher。flatMap 将外层 Publisher 发出的事件中的值传递给内层 Publisher,然后汇总内层 Publisher 给出的事件输出,作为最终变形后的结果。

check("Flat Map 1") {
    [[1, 2, 3], ["a", "b", "c"]]
        .publisher
        .flatMap {
            $0.publisher
        }
}
// Output:
// 1 2 3 a b c

/**
在被订阅后,这个外层 Publisher 会发送两个 Output 事件 (两个事件的值分别是 [1, 2, 3] 和 ["a", "b", "c"]),每个事件的值被 flatMap 传递到内层,并通过 $0.publisher 生成新的 Publisher 并返回。内层 Publisher 实际上是 [1, 2, 3].publisher 和 ["a", "b", "c"].publisher,它们发送的值将被作为 flatMap 的结果,被“展平” (flatten) 后发送出去。
*/
check("Flat Map 2") {
    ["A", "B", "C"]
        .publisher
        .flatMap { letter in
            [1, 2, 3]
                .publisher
                .map { "\(letter)\($0)" }
        }
}
// Output:
// A1 A2 A3 B1 B2 B3 C1 C2 C3

/**
外层 ["A", "B", "C"] 里的每个元素,作为 flatMap 变形闭包的输入,参与到了内层 Publisher 的 map 计算。内层 Publisher 逐次使用 [1, 2, 3] 里的元素,和输入进来的 letter (也就是 “A”, “B” 和 “C”) 进行拼接后作为新事件发出。
*/

reduce

操作完发消息

将数组中的元素按照某种规则进行合并,并得到一个最终的结果。

与 scan 区别:当序列中值耗尽时,它将发布 finished。而经过 reduce 变形后,新的 Publisher 只会在接到上游发出的 finished 事件后,才会将 reduce 后的结果发布出来。而紧接这个结果,则是新的 reduce Publisher 的结束事件。

scan

每一步都发消息

场景:在某个下载任务执行期间,接受 URLSession 的数据回调,将已接收到的数据量做累加来提供一个下载进度条的界面。

同 reduce,但是会记录中途每一步的过程。

与 reduce 区别:一边进行重复操作,一边将每一步中间状态发送出去。


源码类似: 可直接添加到 Sequence extension 中

extension Sequence {
    public func scan(_ initial: ResultElement, _ nextPartialResult: (ResultElement, Element) -> ResultElement) -> [ResultElement] {
        var result: [ResultElement] = []
        for x in self {
            result.append(nextPartialResult(result.last ?? initial, x))
        }
        return result
    }
}

removeDuplicates

移除连续出现的重复事件值

removeDuplicates 经常被用来减少那些非常消耗资源的操作,比如由事件触发造成的网络请求或者图片渲染。如果当作为源头的数据没有改变时,所预期得到的结果也不会变化的话,那么就没有必要去重复这样操作。在源头将重复的事件移除,可以让下游的事件流也变得简单。

check("Remove Duplicates") {
    // removeDuplicates 会处理事件源
    ["S", "Sw", "Sw", "Sw", "Swi",
     "Swif", "Swift", "Swift", "S"] 
        .publisher
        .removeDuplicates()
}
// Output:
// subscription: (["S", "Sw", "Swi", "Swif", "Swift", "S"])

错误处理

Fail

Fail 这个内建的基础 Publisher,它所做的事情就是在被订阅时发送一个错误事件.

Fail(error: .sampleError)

public enum SampleError: Error {
    case sampleError
}

mapError

如果 Publisher 在出错时发送的是 SampleError,但订阅方声明只接受 MyError 时,就算实际上 Publisher 只发出 Output 值而从不会发出 Failure 值,我们也无法使用这个 Subscriber 去接收一个类型不符的 Publisher 的事件。

在这种情况下,我们可以通过使用 mapError 来将 Publisher 的 Failure 转换成 Subscriber 所需要的 Failure 类型

Fail(error: .sampleError)
    .mapError { _ in
        myError.myError
    }

抛出错误

Combine 为 Publisher 的 map 操作提供了一个可以抛出错误的版本,tryMap。使用 tryMap 我们就可以将这类处理数据时发生的错误转变为标志事件流失败的结束事件。

["1", "2", "S", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw MyError.myError
        }
        return value
    }

除了 tryMap 以外,Combine 中还有很多类似的以 try 开头的 Operator,比如 tryScan,tryFilter,tryReduce 等等。当你有需求在数据转换或者处理时,将事件流以错误进行终止,都可以使用对应操作的 try 版本来进行抛出,并在订阅者一侧接收到对应的错误事件。

从错误中恢复

如果我们想要在事件流以错误结束时被转为一个默认值的话,replaceError 就会很有用。

replaceError 会将 Publisher 的 Failure 类型抹为 Never,这正是我们使用 assign 来将 Publisher 绑定到 UI 上时所需要的 Failure 类型。我们可以用 replaceError 来提供这样一个在出现错误时应该显示的默认值。

有一些 Operator 是专门帮助事件流从错误中恢复的,最简单的是 replaceError,它会把错误替换成一个给定的值,并且立即发送 finished 事件

replaceError(中断信号)

["1", "2", "Swift", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw MyError.myError
        }
        return value
    }
    .replaceError(with: -1)
// Output:
// 1 2 -1

catch(中断信号)

catch 则略有不同,它接受的是一个新的 Publisher,当上游 Publisher 发生错误时,catch 操作会使用新的 Publisher 来把原来的 Publisher 替换掉。

["1", "2", "Swift", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw MyError.myError
        }
        return value
    }
    .catch { _ in [-1, -2, -3].publisher }

 // Output:
 // 1 2 -1 -2 -3
 
 // 当错误发生后,原本的 Publisher 事件流将被中断,取而代之,则是由 catch 所提供的事件流继续向后续的 Operator 及 Subscriber 发送事件。原来 Publisher 中的最后一个元素 “4”,将没有机会到达。

组合实现继续输入

如果我们将 (由 ["1", "2", "Swift", "4"] 构成的) 原 Publisher 看作是用户输入,将结果的 Int 看作是最后输出,那么像上面那样的方式使用 replaceError 或者 catch 的话,一旦用户输入了不能转为 Int 的非法值 (如 “Swift”),整个结果将永远停在我们给定的默认恢复值上,接下来的任意用户输入都将被完全忽略。这往往不是我们想要的结果,一般情况下,我们会想要后续的用户输入也能继续驱动输出,这时候我们可以靠组合一些 Operator 来完成所需的逻辑

check("Catch and Continue") {
    ["1", "2", "Swift", "4"].publisher
        .flatMap { s in
            return Just(s)
                .tryMap { s -> Int in
                    guard let value = Int(s) else {
                        throw MyError.myError
                    }
                    return value
                }
                .catch { _ in
                    Just(-1)}
        }
}

// Output:
// 1 2 -1 4

eraseToAnyPublisher

类型抹消

let a = [[1,2,3],[4,5,6]].publisher.flatMap{ $0.publisher }

// 等价于
let a1 = Publishers.FlatMap(upstream: [[1,2,3],[4,5,6]].publisher, maxPublishers: .unlimited) {
    $0.publisher
}

let b = a.map {$0*2}
// 此时 b 的类型为: Publishers.Map, Publishers.Sequence<[[Int]], Never>>, Int>

Combine 提供了 eraseToAnyPublisher 来帮助我们对复杂类型的 Publisher 进行类型抹消,这个方法返回一个 AnyPublisher。

let c = b.eraseToAnyPublisher()
// c: AnyPublisher

Combine 中的其他角色也大都提供了类似的抹消后的类型,比如 AnySubscriber 和 AnySubject 等。在大多数情况下我们都只会关注某个部件所扮演的角色,也即,它到底是一个 Publisher 还是一个 Subscriber。一般我们并不关心具体的类型,因为对 Publisher 的变形往往都伴随着类型的变化。通过类型抹消,可以让事件的传递和订阅操作变得更加简单,对外的 API 也更加稳定。

操作符熔合

将操作符的作用时机提前到创建 Publisher 时的方式,被称为操作符熔合 (operator fusion)。

有时候 map 操作的返回结果的类型并不是 Publishers.Map,比如下面的两个例子:

[1, 2, 3].publisher.map { $0 * 2 }
// Publishers.Sequence<[Int], Never>
Just(10).map { String($0) }
// Just

这是由于 Publishers.Sequence 和 Just 在各自的扩展中对默认的 Publisher 的 map 操作进行了重写。由于 Publishers.Sequence 和 Just 这样的类型在编译期间我们就能确定它们在被订阅时就会同步地发送所有事件,所以可以将 map 的操作直接作用在输入上,而不需要等待每次事件发生时再去操作。

可以想象 Publishers.Sequence 上 map 操作的实现:

extension Publishers.Sequence {
public func map(_ transform: (Elements.Element) -> T) -> Publishers.Sequence<[T], Failure> {
    return Publishers.Sequence(sequence: sequence.map(transform))
    }
}

这样做可以避免 transform 的 @escaping 的要求,避免存储这个变形闭包,让所得到的 Publisher 更加高效,同时也让后续 Publisher 变形类型能简单一些。这些优点在多次变形的时候尤为突出。

你可能感兴趣的:(Combine Publisher详解)