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

Sma*_*alk 14 asynchronous swift combine

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

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

前任。简单的 PromiseKit 链式异步调用语法

getAccessCodeFromSyncProvider.then{accessCode in startSync(accessCode)}.then{popToRootViewController}.catch{handleError(error)}
Run Code Online (Sandbox Code Playgroud)

我明白:

async/await 的 Swift 标准库实现将解决这个问题(async/await 尚不存在,尽管Chris Latter 本人有很多喋喋不休和参与

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

flatMap 可用于链接 Futures

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

  1. 如果我将 Futures 包装在一个方法中,sink以处理结果,则该方法似乎在订阅者被调用之前超出了范围sink

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

问题:

  1. 我是否必须保留调用方法之外的每个发布者和订阅者
  2. 如何使用 Swift 标准库复制简单的链式异步,然后将其嵌入到我可以按需调用以从顶部重新启动链式异步调用的 swift 实例方法中?
//how is this done using Combine?
func startSync() {
 getAccessCodeFromSyncProvider.then{accessCode in startSync(accessCode)}.catch{\\handle error here}
}
Run Code Online (Sandbox Code Playgroud)

mat*_*att 21

这不是你整个问题的真正答案——只是关于如何开始使用 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>
Run Code Online (Sandbox Code Playgroud)

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

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

那么会发生什么呢?

start
finished 1 // 3 seconds later
finished 2 // 3 seconds later
done
Run Code Online (Sandbox Code Playgroud)

所以你可以看到我们链接了两个异步操作,每个操作需要 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>
}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,我们的 Future 发布者的想法只需要传递promise回调;他们实际上不必是打电话给他们的人。一个promise回调由此可以随时随地调用,所以我们将不会继续在那之前。

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

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

稍微整洁一点

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

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 = Deferred{Future<Bool,Error> { promise in
        self.async1(promise)
    }}
    let f2 = Deferred{Future<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>
}
Run Code Online (Sandbox Code Playgroud)


Sen*_*ful 9

马特的答案是正确的,用于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>
}
Run Code Online (Sandbox Code Playgroud)

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

func async2() -> AnyPublisher<Bool, Error> {
  Future { promise in
    delay(3) {
      print("async2")
      promise(.success(true))
    }
  }.eraseToAnyPubilsher()
}
Run Code Online (Sandbox Code Playgroud)