如何使用 Combine + Swift 复制 PromiseKit 风格的链式异步流

Posted

技术标签:

【中文标题】如何使用 Combine + Swift 复制 PromiseKit 风格的链式异步流【英文标题】:How to replicate PromiseKit-style chained async flow using Combine + Swift 【发布时间】:2020-04-13 03:09:00 【问题描述】:

在 Xcode 11 beta 破解 PK v7 之前,我在一个项目中成功使用了 PromiseKit。为了减少外部依赖,我决定废弃 PromiseKit。处理链式异步代码的最佳替代品似乎是使用新的 Combine 框架的 Futures。

我正在努力使用 Combine 来复制简单的 PK 语法

例如。简单的 PromiseKit 链式异步调用语法

getAccessCodeFromSyncProvider.thenaccessCode in startSync(accessCode).thenpopToRootViewController.catchhandleError(error)

我明白了:

async/await 的 Swift 标准库实现将解决这个问题(async/await 尚不存在,尽管来自 Chris Latter himself 的大量讨论和参与)

我可以使用信号量进行复制(容易出错?

flatMap 可用于链接 Futures

我想要的异步代码应该能够按需调用,因为它涉及确保用户登录。我正在努力解决两个概念问题。

    如果我将 Futures 包装在一个方法中,并使用 sink 来处理结果,那么在 sink 调用订阅者之前,该方法似乎超出了范围。

    由于 Futures 只执行一次,我担心如果我多次调用该方法,我只会从第一次调用中得到旧的、陈旧的结果。要解决这个问题,也许我会使用 PassthroughSubject?这允许按需调用 Publisher。

问题:

    我是否必须保留所有发布者和订阅者 调用方法 如何使用 Swift 标准库复制简单的链式异步,然后将其嵌入到我可以按需调用以从顶部重新启动链式异步调用的快速实例方法中??
//how is this done using Combine?
func startSync() 
 getAccessCodeFromSyncProvider.thenaccessCode in startSync(accessCode).catch\\handle error here

【问题讨论】:

极其广泛和广泛。你能把问题集中在下面吗?至少解释你的代码的目标。假设我们不知道您的任何方法的作用。你说“这是怎么做到的”,但“这个”是什么? 马特,爱你的书!!当我第一次学习时,它们很重要。我将尝试使用特定的代码流来简化问题。作为第一个切入点,我想说我正在尝试在 Swift 中实现最简单的异步/等待形式,在最高抽象级别上,而不依赖于像 PromiseKit 这样的第三方库。 PK 有我想复制的美妙语法。我使用 PK 的代码读起来有点像“firstlyasync.thenasync.recoverasync.doneclean-up.catchhandle errors”。自我记录,易于推理。这是我的目标,只使用 Swift 标准库。 啊哈!好吧,Combine 不是 PromiseKit,恐怕。您可以肯定地链接异步事物,但它不会是相同的。 【参考方案1】:

这不是您整个问题的真正答案——只是关于如何开始使用 Combine 的部分。我将演示如何使用 Combine 框架链接两个异步操作:

    print("start")
    Future<Bool,Error>  promise in
        delay(3) 
            promise(.success(true))
        
    
    .handleEvents(receiveOutput: _ in print("finished 1"))
    .flatMap _ in
        Future<Bool,Error>  promise in
            delay(3) 
                promise(.success(true))
            
        
    
    .handleEvents(receiveOutput: _ in print("finished 2"))
    .sink(receiveCompletion: _ in, receiveValue: _ in print("done"))
        .store(in:&self.storage) // storage is a persistent Set<AnyCancellable>

首先,你的持久化问题的答案是:最终订阅者必须持久化,而做到这一点的方法是使用.store 方法。通常,您将拥有 Set&lt;AnyCancellable&gt; 作为属性,就像这里一样,您只需调用 .store 作为管道中的最后一件事,将您的订阅者放入其中。

接下来,在这个管道中,我使用.handleEvents 只是为了在管道移动时给自己一些打印输出。这些只是诊断,在实际实现中不存在。所有print 声明都是纯粹的,所以我们可以谈论这里发生的事情。

那么会发生什么?

start
finished 1 // 3 seconds later
finished 2 // 3 seconds later
done

所以你可以看到我们已经链接了两个异步操作,每个操作需要 3 秒。

我们是怎么做到的?我们从 Future 开始,它必须在完成时调用传入的 promise 方法,并使用 Result 作为完成处理程序。之后,我们使用.flatMap 产生另一个 Future 并投入运行,再次做同样的事情。

所以结果并不漂亮(如 PromiseKit),但它是一个异步操作链。

在使用 Combine 之前,我们可能已经使用某种 Operation / OperationQueue 依赖项来完成此操作,这样可以正常工作,但 PromiseKit 的直接可读性会更少。

更真实一点

说了这么多,这里有一个更现实的重写:

var storage = Set<AnyCancellable>()
func async1(_ promise:@escaping (Result<Bool,Error>) -> Void) 
    delay(3) 
        print("async1")
        promise(.success(true))
    

func async2(_ promise:@escaping (Result<Bool,Error>) -> Void) 
    delay(3) 
        print("async2")
        promise(.success(true))
    

override func viewDidLoad() 
    print("start")
    Future<Bool,Error>  promise in
        self.async1(promise)
    
    .flatMap _ in
        Future<Bool,Error>  promise in
            self.async2(promise)
        
    
    .sink(receiveCompletion: _ in, receiveValue: _ in print("done"))
        .store(in:&self.storage) // storage is a persistent Set<AnyCancellable>

如您所见,我们未来发布者的想法只需传递promise 回调即可;他们实际上不必是打电话给他们的人。因此可以在任何地方调用promise 回调,直到那时我们才会继续。

因此,您可以很容易地看到如何用真正的异步操作替换人工的delay,该操作以某种方式持有这个promise 回调并且可以在它完成时调用它。另外我的承诺结果类型纯粹是人为的,但你可以再次看到它们如何用于在管道中传达有意义的东西。当我说promise(.success(true)) 时,这会导致true 弹出管道的末端;我们在这里忽略了这一点,但它可能是某种彻头彻尾的有用价值,甚至可能是下一个未来。

(还请注意,我们可以在链中的任何位置插入.receive(on: DispatchQueue.main),以确保紧随其后的内容在主线程上启动。)

稍微整洁

我还想到,通过将 Future 发布者移到常量中,我们可以使语法更整洁,也许更接近 PromiseKit 的可爱简单链。但是,如果您这样做,您可能应该将它们包装在延迟发布者中,以防止过早评估。比如:

var storage = Set<AnyCancellable>()
func async1(_ promise:@escaping (Result<Bool,Error>) -> Void) 
    delay(3) 
        print("async1")
        promise(.success(true))
    

func async2(_ promise:@escaping (Result<Bool,Error>) -> Void) 
    delay(3) 
        print("async2")
        promise(.success(true))
    

override func viewDidLoad() 
    print("start")
    let f1 = DeferredFuture<Bool,Error>  promise in
        self.async1(promise)
    
    let f2 = DeferredFuture<Bool,Error>  promise in
        self.async2(promise)
    
    // this is now extremely neat-looking
    f1.flatMap _ in f2 
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: _ in, receiveValue: _ in print("done"))
        .store(in:&self.storage) // storage is a persistent Set<AnyCancellable>

【讨论】:

谢谢马特!,我对 .map 与 .flatMap 的作用仍然有些模糊,但我会好好学习的。我唯一没有看到的是.receiveOn。 (当 .receiveCompletion 没有在主线程上完成时,我会发生奇怪的错误。)但我会在操场上测试上述内容。谢谢你给我一个跑步的开始。必须知道对最终订阅者的持久引用是有帮助的。 (我认为我的第一次尝试失败了,因为在发布者完成异步调用之前调用包含订阅者的方法超出范围时订阅者被自动取消。)让我在✔之前玩。 Receive(on) 太简单了,我没有把它放进去,这个例子也不需要它。如果你喜欢,我会添加它。 另外,我也懒得去捕捉错误,因为有这么多种方法来处理错误,没有必要对此事进行偏见。我的sink 实际上确实捕获了错误,但它目前忽略了它们。但是您可以改为映射错误,捕获并发出不是错误的内容,等等。 顺便说一句,我刚刚取消了map 的使用,我傻了,你可以直接flatMap 到下一个发布者。掌心。 没什么精英,我就是这样学东西的! — 我在答案中添加了另一部分,展示了如何通过将 Future 发布者移动到本地常量来整理管道的实际表达式。正如你所说的,Combine 永远不会是 PromiseKit,但它可能是我们在不求助于第三方库的情况下最接近的,我认为你走这条路是对的。【参考方案2】:

matt's answer 是正确的,使用flatMap 链接承诺。我在使用 PromiseKit 时养成了返回 Promise 的习惯,并将其带到了 Combine(返回 Futures)。

我发现它使代码更易于阅读。这是马特的最后一个建议的例子:

var storage = Set<AnyCancellable>()

func async1() -> Future<Bool, Error> 
  Future  promise in
    delay(3) 
      print("async1")
      promise(.success(true))
    
  


func async2() -> Future<Bool, Error> 
  Future  promise in
    delay(3) 
      print("async2")
      promise(.success(true))
    
  


override func viewDidLoad() 
  print("start")

  async1()
    .flatMap  _ in async2() 
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: _ in, receiveValue: _ in print("done"))
    .store(in:&self.storage) // storage is a persistent Set<AnyCancellable>

请注意,AnyPublisher 也将用作返回值,因此您可以抽象出 Future 并让它返回 AnyPublisher&lt;Bool, Error&gt;

func async2() -> AnyPublisher<Bool, Error> 
  Future  promise in
    delay(3) 
      print("async2")
      promise(.success(true))
    
  .eraseToAnyPubilsher()

【讨论】:

这提供了一个很好的起点,并回答了问题。【参考方案3】:

如果你想使用类似 PromiseKit 的语法,这里有一些 Publisher 的扩展

我正在使用它从 PromiseKit 无缝切换到项目中的组合

extension Publisher 
    
    func then<T: Publisher>(_ closure: @escaping (Output) -> T) -> Publishers.FlatMap<T, Self>
    where T.Failure == Self.Failure 
        flatMap(closure)
    
    
    func asVoid() -> Future<Void, Error> 
        return Future<Void, Error>  promise in
            let box = Box()
            let cancellable = self.sink  completion in
                if case .failure(let error) = completion 
                    promise(.failure(error))
                 else if case .finished = completion 
                    box.cancellable = nil
                
             receiveValue:  value in
                promise(.success(()))
            
            box.cancellable = cancellable
        
    
    
    @discardableResult
    func done(_ handler: @escaping (Output) -> Void) -> Self 
        let box = Box()
        let cancellable = self.sink(receiveCompletion: compl in
            if case .finished = compl 
                box.cancellable = nil
            
        , receiveValue: 
            handler($0)
        )
        box.cancellable = cancellable
        return self
    
    
    @discardableResult
    func `catch`(_ handler: @escaping (Failure) -> Void) -> Self 
        let box = Box()
        let cancellable = self.sink(receiveCompletion:  compl in
            if case .failure(let failure) = compl 
                handler(failure)
             else if case .finished = compl 
                box.cancellable = nil
            
        , receiveValue:  _ in )
        box.cancellable = cancellable
        return self
    
    
    @discardableResult
    func finally(_ handler: @escaping () -> Void) -> Self 
        let box = Box()
        let cancellable = self.sink(receiveCompletion:  compl in
            if case .finished = compl 
                handler()
                box.cancellable = nil
            
        , receiveValue:  _ in )
        box.cancellable = cancellable
        return self
    


fileprivate class Box 
    var cancellable: AnyCancellable?

下面是一个使用示例:

func someSync() 
    Future<Bool, Error>  promise in
        delay(3) 
            promise(.success(true))
        
    
    .then  result in
        Future<String, Error>  promise in
            promise(.success("111"))
        
    
    .done  string in
        print(string)
    
    .catch  err in
        print(err.localizedDescription)
    
    .finally 
        print("Finished chain")
    

【讨论】:

刚刚开始探索 PromiseKit,与 Combine 进行比较,想知道采用任何非 Apple 框架是否有价值——或者,只使用 Combine。有明显的区别,但我很好奇......上面的内容+Combine 和 Swift 5.5(异步,actors)的未来(很快)...... PromiseKit 提供了很多优势吗? (我隐约知道有相当多的 PK 扩展提供 ios 特定的功能,但是......不熟悉它们,所以想知道你的意见可能是什么)。我注意到你写的“从 PK 无缝切换到组合”。似乎表明你正在放弃PK。 PromiseKit 在Combine 中比Future 有几个优势。例如,对于每个 Promise,您可以通过枚举属性“value”突出显示值或错误。结合将不得不写一个扩展。但是Combine的主要优势在于它是一个原生框架,你不再需要注入第三方依赖来简化应用程序中的异步工作。 另外,对于任何 iOS 功能,如果需要一系列值,完全可以通过 Future 或 Publisher 为 Combine 编写扩展。【参考方案4】:

您可以将此框架用于 Swift 协程,它也可以与 Combine 一起使用 - https://github.com/belozierov/SwiftCoroutine

DispatchQueue.main.startCoroutine 
    let future: Future<Bool, Error>
    let coFuture = future.subscribeCoFuture()
    let bool = try coFuture.await()


【讨论】:

您的个人资料表明您与已链接的网站相关联。 不透露它是你的而链接到你所属的东西(例如图书馆、工具、产品或网站)在 Stack Overflow 上被视为垃圾邮件。请参阅:What signifies "Good" self promotion?、some tips and advice about self-promotion、What is the exact definition of "spam" for Stack Overflow? 和 What makes something spam。

以上是关于如何使用 Combine + Swift 复制 PromiseKit 风格的链式异步流的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 中使用 Combine 读取 JSON 错误对象的属性值?

如何在 Swift Combine 中创建自定义链?

如何通过另一个可观察对象观察已发布的属性 - Swift Combine

Swift Combine:如何从发布者列表中创建单个发布者?

使用 Swift 和 Combine 链接 + 压缩多个网络请求

Swift Combine - 如何获得一个发布者,为 UITextField 的文本属性的每个字符更改提供事件