SwiftUI开发总结(四)再聊聊关于combine的tips

2,079 阅读6分钟

在篇目一中,我们简单讲述了如何使用combine,但是其真实的使用场景相对灵活多变,因此针对不同使用场景,采取不同的使用方法,就需要我们掌握更多的使用方法,以下是几个combine的常用方法,非常好用,建议收藏。

组合多个Publisher的结果

Publisher最常用的方式就是获取值后进行通知回调,然后进行计算操作。单个的回调,我们在篇目一中讲解过,这里就不多做介绍了。有些场景下,我们需要等待两个变量都获取值后,再进行操作,比如我个人开发的满满财表,需要等待汇率跟现金都获取到再汇总计算。

针对于这个问题,通常有两种解决办法:

zip

以下是zip的使用方法:

let publisher1 = URLSession.shared.dataTaskPublisher(for: url1)
let publisher2 = URLSession.shared.dataTaskPublisher(for: url2)
​
Publishers.Zip(publisher1, publisher2)
    .sink { completion in
        // 处理完成事件
    } receiveValue: { data1, data2 in
        // 处理两个Publisher的结果
    }
​
CombineLatest

以下是CombineLatest的使用方法:

let publisher1 = URLSession.shared.dataTaskPublisher(for: url1)
let publisher2 = URLSession.shared.dataTaskPublisher(for: url2)
​
Publishers.CombineLatest(publisher1, publisher2)
    .sink { completion in
        // 处理完成事件
    } receiveValue: { data1, data2 in
        // 处理两个Publisher的结果
    }
区别

迷糊没??没错,这两个方法在使用上,一摸一样,那么该如何选择呢?

CombineLatest

  • 当任何一个源发布者发出新值时,将获取所有源发布者的最新值并进行组合。
  • 生成的新发布者将按照最近接收到的值来更新。
  • 发布者之间的值可以不完全对应,只要至少有一个发布者有值,就可以进行组合。

Zip

  • 等待所有源发布者都发出一个新值后,才进行组合。
  • 组合的值将以元组的形式呈现,包含所有源发布者的最新值。
  • 发布者之间的值必须严格一一对应,否则组合将等待所有发布者都具有对应的值。

也就是说,如果我们其中一个数据源改变就需要进行回调操作,那么我们就应该选择使用CombineLatest,如果我们的使用场景是两个数据源的数据必须全部修改才进行回调,那么我们就应该选择使用Zip

定时操作和超时处理

有些场景下,数据源的数据并非需要及时回调,这个时候我们就需要采用延时处理,方法如下:

let publisher = ...
​
let timeoutInterval: TimeInterval = 5.0
​
publisher
    .timeout(timeoutInterval, scheduler: DispatchQueue.main, options: nil) {
        // 在超时时执行的闭包
    }
    .sink { completion in
        // 处理完成事件
    } receiveValue: { value in
        // 处理结果值
    }
​

在上述示例中,使用timeout操作符设置一个超时时间,如果在指定时间内没有收到新的值,将执行提供的闭包。这可以用于处理网络请求等需要设定超时的场景。

条件切换

import Combine
​
enum Condition {
    case trueCondition
    case falseCondition
}
​
let conditionPublisher = CurrentValueSubject<Condition, Never>(.trueCondition)
​
let truePublisher = Just("True Publisher")
let falsePublisher = Just("False Publisher")
​
conditionPublisher
    .flatMap { condition -> AnyPublisher<String, Never> in
        switch condition {
        case .trueCondition:
            return truePublisher.eraseToAnyPublisher()
        case .falseCondition:
            return falsePublisher.eraseToAnyPublisher()
        }
    }
    .sink { value in
        print("Received value: (value)")
    }
​

在上述示例中,我们使用CurrentValueSubject作为条件发布者,并初始化为.trueCondition。我们定义了两个简单的Just发布者,分别表示条件为truefalse时的结果。

然后,我们使用flatMap操作符将conditionPublisher的条件值映射到相应的发布者,并使用eraseToAnyPublisher将结果转换为AnyPublisher类型。这样,我们就可以将两个不同的发布者进行组合。

最后,我们通过sink订阅了组合后的发布者,并在接收到值时进行打印。

我们可以根据需要修改条件和结果的类型,并根据具体的业务逻辑和场景定义自己的条件发布者和结果发布者。

自定义操作符

通常情况下,系统提供的combine操作符就可以满足大部分的操作需求,但是如果在实际开发中遇到了以下场景:

  1. 组合多个发布者的值:当您需要将多个发布者的值进行合并、组合或转换时,可以使用自定义操作符。例如,将两个发布者的值进行合并、根据条件过滤值、对值进行转换等。
  2. 封装常用的操作序列:如果您经常使用一系列的操作符来处理发布者的值,您可以将这些操作符封装为一个自定义操作符,以便在代码中重复使用。这样可以提高代码的可读性和可维护性。
  3. 创建特定功能的操作符:自定义操作符允许您根据自己的需求创建特定功能的操作符。例如,如果您需要执行一些特定的数据处理、过滤、聚合或转换操作,您可以定义一个自定义操作符来满足这些需求。
  4. 扩展Combine框架的功能:自定义操作符是扩展Combine框架功能的强大工具。您可以使用自定义操作符来填补Combine框架中缺少的某些操作符或功能,以满足特定的需求。

自定义操作符是Combine框架的一个非常强大的功能,它可以帮助您根据自己的需求扩展Combine框架的功能。自定义操作符是通过实现Publisher协议的扩展来实现的。

下面是一个简单的例子,展示了如何创建一个自定义操作符,将两个发布者的值进行合并:

swiftCopy code
struct MergeOperator<Upstream1: Publisher, Upstream2: Publisher>: Publisher {
    typealias Output = Upstream1.Output
    typealias Failure = Upstream1.Failure
    
    let upstream1: Upstream1
    let upstream2: Upstream2
    
    init(upstream1: Upstream1, upstream2: Upstream2) {
        self.upstream1 = upstream1
        self.upstream2 = upstream2
    }
    
    func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
        let subscription = MergeOperatorSubscription(subscriber: subscriber)
        upstream1.subscribe(subscription)
        upstream2.subscribe(subscription)
    }
}
​
class MergeOperatorSubscription<SubscriberType: Subscriber>: Subscription where SubscriberType.Input == Int {
    private var subscriber: SubscriberType?
    
    init(subscriber: SubscriberType) {
        self.subscriber = subscriber
    }
    
    func request(_ demand: Subscribers.Demand) {
        // Do nothing
    }
    
    func cancel() {
        subscriber = nil
    }
    
    func receive(_ input: Int) {
        _ = subscriber?.receive(input)
    }
}
​
extension Publisher {
    func merge<Other: Publisher>(with other: Other) -> MergeOperator<Self, Other> {
        return MergeOperator(upstream1: self, upstream2: other)
    }
}
let publisher1 = Just(1)
let publisher2 = Just(2)
​
let mergedPublisher = publisher1.merge(with: publisher2)
​
let subscription = mergedPublisher
    .sink { value in
        print("Received merged value: (value)")
    }

在上面的例子中,我们首先定义了一个MergeOperator类型,它实现了Publisher协议。这个类型包含两个泛型参数Upstream1Upstream2,分别表示要合并的两个发布者类型。MergeOperator类型还定义了OutputFailure类型,它们分别表示合并后的发布者将要发布的值类型和错误类型。

MergeOperator类型实现了receive(subscriber:)方法,该方法接收一个订阅者,并将订阅者注册到要合并的两个发布者中。此外,MergeOperator类型还实现了一个内部的MergeOperatorSubscription类,它实现了Subscription协议,它的作用是接收来自要合并的两个发布者的值,并将这些值转发给订阅者。

最后,我们在Publisher协议的扩展中实现了自定义操作符merge(with:),该操作符接收一个要合并的发布者,并将它们传递给MergeOperator类型的构造函数。