Как конвертировать в / из AnyPublisher и AnyPublisher ? - PullRequest
1 голос
/ 12 июля 2020

Я обертываю асинхронные c запросы в издателях Combine, чтобы их можно было легко использовать в разных конвейерах.

Потребитель может удерживать этих издателей следующим образом:

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

Два наиболее распространенных типа запросов:

  1. Активировать и забыть (завершить немедленно) : например, сохранение значения в пользовательских настройках по умолчанию может быть смоделировано как пожар. и забыть. Пока что кажется, что AnyPublisher<Never, Never> - хороший способ express этого типа. Это может быть легко создано с помощью Empty<Never, Never>(completeImmediately: true).
  2. Запускать, ждать и игнорировать результат : Примером этого является сохранение значения в базе данных (и игнорирование результата), но все же желая дождаться завершения сохранения, прежде чем продолжить конвейер. Я использовал 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, дождитесь завершения Void

    Поскольку для этого есть встроенный оператор, преобразование выполняется легко:

    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, завершить немедленно) без необходимости вызывать как map, так и append? (Например, аналогично тому, как ignoreOutput использовалось для решения второго преобразования?)
  2. Есть ли лучший способ выполнить третье преобразование (Void -> Never, завершить немедленно), для которого не требуется cancellables?

1 Ответ

0 голосов
/ 12 июля 2020

Я не нашел более элегантного способа сделать №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()
    }
  }
}

...