Swift 合并:将错误转化为发布者的输出

Swift Combine: turn error into the publisher's output

我正在尝试重构一些代码,我目前正在将 Combine 发布者与回调闭包混合在一起。

该代码向 REST 端点发出登录请求。失败时,我希望函数的调用者接收错误有效负载(挑战),成功时,调用者不需要做任何事情。所以这是一种奇怪的情况,调用者只对错误感兴趣。

目前我是这样做的(简化代码):

import Foundation
import Combine

struct Challenge {
  let id: Int
}

struct MyError: Error {
  let challenge: Challenge?
}

class UserManager {
  @Published var user: String?

  private func doRequest(username: String, password: String) -> AnyPublisher<String, MyError> {
    // In the real world this does a request to a REST API, returning either a User object or an error containing a Challenge.
    return Fail(outputType: String.self, failure: MyError(challenge: Challenge(id: 1)))
      .eraseToAnyPublisher()
  }

  func login(username: String, password: String, next: @escaping (Challenge) -> ()) -> AnyCancellable {
    doRequest(username: username, password: password)
      .catch { error -> AnyPublisher<String, Never> in
        if let challenge = error.challenge {
          next(challenge)
        }
        return Empty().eraseToAnyPublisher()
      }
      .sink(
        receiveValue: { [weak self] user in
          self?.user = user
        }
      )
  }
}

let userManager = UserManager()
var anyCancellables = Set<AnyCancellable>()

userManager.login(username: "hello", password: "world") { challenge in
  print("received challenge!")
}
.store(in: &anyCancellables)

如您所见,login 函数接受一个名为 next 的参数,发布者自己处理 sink 运算符中的值。将发布者与这样的闭包组合起来感觉不太好,理想情况下我希望 login 函数具有此签名:

func login(username: String, password: String) -> AnyPublisher<Challenge, Never>

但是当我想写这样一个函数时,我 运行 遇到了我不能在 login 函数本身中使用 sink 的问题,因为这会转换return 输入 AnyCancellable。基本上我想将错误转化为发布者的输出,并处理 login 函数中的实际值。这怎么可能?

好吧,我想有时候只需要写下一个问题就可以解决它

func login(username: String, password: String) -> AnyPublisher<Challenge, Never> {
  doRequest(username: username, password: password)
    .compactMap {
      self.user = [=10=]
      return nil
    }
    .catch { error -> AnyPublisher<Challenge, Never> in
      if let challenge = error.challenge {
        return Just(challenge).eraseToAnyPublisher()
      }
      return Empty().eraseToAnyPublisher()
    }
    .eraseToAnyPublisher()
}

不完全确定使用这样的 compactMap 来处理副作用是否真的是最好的方法,但它确实有效。