如何转换to/from AnyPublisher<Void, Never>和AnyPublisher<Never, Never>?

How to convert to/from AnyPublisher<Void, Never> and AnyPublisher<Never, Never>?

我将异步请求包装在 Combine 发布者中,以便它们可以轻松地跨不同的管道使用。

消费者可能会按如下方式保留这些发布者:

struct Dependencies {
  var loadImageRequest: AnyPublisher<UIImage, Never>
  var saveToDatabaseRequest: AnyPublisher<Void, Never>
  var saveToUserDefaultsRequest: AnyPublisher<Never, Never>
}

两种更常见的请求类型是:

  1. 即发即弃(立即完成):例如,将值保存到用户默认值,可以建模为即发即弃。到目前为止, AnyPublisher<Never, Never> 似乎是表达这种类型的好方法。这可以通过 Empty<Never, Never>(completeImmediately: true).
  2. 轻松构建
  3. 触发、等待并忽略结果:一个例子是将值保存到数据库(并忽略结果),但仍想等到保存完成在继续管道之前完成。我一直在使用 AnyPublisher<Void, Never> 来模拟这些请求类型。构造这些的一种简单方法是通过 Future<Void, Never>() { promise in promise(.success(()))}.

这两个都有忽略结果的共同主题。因此,在将这些交给消费者时,有时在这两种数据类型之间进行转换很有用:AnyPublisher<Never, Never>AnyPublisher<Void, Never>.

可以通过三种方式在两者之间进行转换:

  1. Never -> Void,立即完成

    一种转换方法是强制转换:

    let neverPublisher: AnyPublisher<Never, Never> = ...
    let voidPublisher: AnyPublisher<Void, Never> = neverPublisher
      .map { _ in () }
      .append(Just(()))
      .eraseToAnyPublisher()
    
  2. Void -> Never,等待虚空完成

    因为这个有一个内置运算符,所以转换很容易:

    let voidPublisher: AnyPublisher<Void, Never> = ...
    let neverPublisher: AnyPublisher<Never, Never> = voidPublisher
      .ignoreOutput()
      .eraseToAnyPublisher()
    
  3. Void -> Never,立即完成

    我不确定进行此转换的最佳方法。我提出的解决方案有两个主要缺点:使用 handleEvents,并且需要在某处定义 cancellables

    let cancellables: Set<AnyCancellable> = []
    let voidPublisher: AnyPublisher<Void, Never> = ...
    let neverPublisher: AnyPublisher<Never, Never> = Empty<Never, Never>(completeImmediately: true)
      .handleEvents(receiveCompletion: { _ in voidPublisher.sink(receiveValue: { _ in }).store(in: &cancellables) })
      .eraseToAnyPublisher()
    

问题:

  1. 是否有更好的方法来执行转换 #1(Never -> Void,立即完成)而不需要同时调用 mapappend? (例如类似于ignoreOutput是如何解决第二次转换的?)
  2. 有没有更好的方法来进行不需要cancellables的第三次转换(Void -> Never,立即完成)?

我还没有找到更优雅的方法来执行 #2。

这是更好的#3(Void -> Never,立即完成)解决方案:

let voidPublisher: AnyPublisher<Void, Never> = ...
let neverPublisher: AnyPublisher<Never, Never> = Publishers.Merge(Just<Void>(()), saveToDatabase)
  .first()
  .ignoreOutput()
  .eraseToAnyPublisher()

这里是 我用来确保一切都按预期运行的方法:

import Combine
import Foundation

let delaySec: TimeInterval = 0.1
let halfDelaySec: TimeInterval = delaySec / 2
let halfDelayMicroSeconds: useconds_t = useconds_t(halfDelaySec * 1_000_000)
let sleepBufferMicroSeconds: useconds_t = useconds_t(0.01 * 1_000_000)

var cancellables = [AnyCancellable]()
var output: [String] = []

func performVoidAction(voidPublisher: AnyPublisher<Void, Never>) {
  voidPublisher
    .handleEvents(
      receiveCompletion: { _ in output.append("performVoidAction - completion") },
      receiveCancel: { output.append("performVoidAction - cancel") })
    .sink(receiveValue: { output.append("performVoidAction - sink") })
    .store(in: &cancellables)
}

func performNeverAction(neverPublisher: AnyPublisher<Never, Never>) {
  neverPublisher
    .handleEvents(
      receiveCompletion: { _ in output.append("performNeverAction - completion") },
      receiveCancel: { output.append("performNeverAction - cancel") })
    .sink(receiveValue: { _ in output.append("performNeverAction - sink") })
    .store(in: &cancellables)
}

func makeSaveToDatabasePublisher() -> AnyPublisher<Void, Never> {
  Deferred { _saveToDatabase() }.eraseToAnyPublisher()
}

func makeSaveToUserDefaultsPublisher() -> AnyPublisher<Never, Never> {
  Deferred { _saveToUserDefaults() }.eraseToAnyPublisher()
}

// --->(Void)|
// AnyPublisher<Void, Never> wraps an API that does something and finishes upon completion.
private func _saveToDatabase() -> AnyPublisher<Void, Never> {
  return Future<Void, Never> { promise in
    output.append("saving to database")
    DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: DispatchTime.now() + delaySec) {
      output.append("saved to database")
      promise(.success(()))
    }
  }.eraseToAnyPublisher()
}

// |
// AnyPublisher<Never, Never> wraps an API that does something and completes immediately, it does not wait for completion.
private func _saveToUserDefaults() -> AnyPublisher<Never, Never> {
  output.append("saved to user defaults")
  return Empty<Never, Never>(completeImmediately: true)
    .eraseToAnyPublisher()
}

func assert(_ value: Bool) -> String {
  value ? "✅" : "❌"
}

// tests
assert(output.isEmpty)

var saveToDatabase = makeSaveToDatabasePublisher()
assert(output.isEmpty, "It should not fire the action yet.")

// verify database save, first time
performVoidAction(voidPublisher: saveToDatabase)
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty)

// verify database save, second time
performVoidAction(voidPublisher: saveToDatabase)
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty)

var saveToUserDefaults = makeSaveToUserDefaultsPublisher()
assert(output.isEmpty, "It should not fire the action yet.")

// verify user defaults save, first time
performNeverAction(neverPublisher: saveToUserDefaults)
assert(!output.isEmpty && output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output

// verify user defaults save, second time
performNeverAction(neverPublisher: saveToUserDefaults)
assert(!output.isEmpty && output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output

// MARK: - Problem: AnyPublisher<Never, Never> -> AnyPublisher<Void, Never>

// MARK: Solution 1
// `|` ➡️ `(Void)|`

performVoidAction(
  voidPublisher: saveToUserDefaults
    .map { _ in () }
    .append(Just(()))
    .eraseToAnyPublisher())
assert(output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output"


// MARK: - Problem: AnyPublisher<Void, Never> -> AnyPublisher<Never, Never>

// MARK: Solution 2 (Wait)
// `--->(Void)|` ➡️ `--->|`

performNeverAction(
  neverPublisher: saveToDatabase.ignoreOutput().eraseToAnyPublisher())
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty)


// MARK: Solution 3 (No wait)
// `--->(Void)|` ➡️ `|`

performNeverAction(
  neverPublisher: Publishers.Merge(Just<Void>(()), saveToDatabase)
    .first()
    .ignoreOutput()
    .eraseToAnyPublisher())
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(output.isEmpty)

print("done")

最后,作为 Publisher 的运营商解决方案如下:

extension Publisher where Output == Never {
  func asVoid() -> AnyPublisher<Void, Failure> {
    self
      .map { _ in () }
      .append(Just(()).setFailureType(to: Failure.self))
      .eraseToAnyPublisher()
  }
}

extension Publisher where Output == Void {
  func asNever(completeImmediately: Bool) -> AnyPublisher<Never, Failure> {
    if completeImmediately {
      return Just<Void>(())
        .setFailureType(to: Failure.self)
        .merge(with: self)
        .first()
        .ignoreOutput()
        .eraseToAnyPublisher()
    } else {
      return self
        .ignoreOutput()
        .eraseToAnyPublisher()
    }
  }
}