Как обернуть шаблон делегата одноразовым издателем?

Обычно мы можем связать наш асинхронный код и объединить, заключив наш асинхронный код в одноразовый издатель, используя Future:

func readEmail() -> AnyPublisher<[String], Error> {
  Future { promise in
    self.emailManager.readEmail() { result, error in
      if let error = error {
        promise(.failure(error))
      } else {
        promise(.success(result))
      }
    }
  }.eraseToAnyPublisher()
}

С другой стороны, если мы обертываем шаблон делегата (вместо асинхронного обратного вызова), это рекомендуется использовать PassthroughSubject, поскольку методы могут запускаться несколько раз:

final class LocationHeadingProxy: NSObject, CLLocationManagerDelegate {

  private let headingPublisher: PassthroughSubject<CLHeading, Error> 

  override init() {
    headingPublisher = PassthroughSubject<CLHeading, Error>()
    // ...
  }

  func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    headingPublisher.send(newHeading) 
  }
}

Однако я пытаюсь создать одноразового издателя который обертывает существующий шаблон делегата. Причина в том, что я запускаю такой метод, как connect(), и ожидаю, что либо успех, либо неудача произойдут немедленно. Я не хочу, чтобы будущие обновления повлияли на конвейер.

Например, представьте, что я использую WKExtendedRuntimeSession и заключил метод .start() в startSession() ниже. Если я успешно упаковал это, я смогу использовать его так:

manager.startSession()
  .sink(
    receiveCompletion: { result in
      if result.isError {
        showFailureToStartScreen()
      }
    },
    receiveValue: { value in
      showStartedSessionScreen()
    })
  .store(in: &cancellables)

Причина, по которой одноразовый издатель полезен, заключается в том, что мы ожидаем, что один из следующих двух методов будет вызван вскоре после вызова метода:

  • Успех: extendedRuntimeSessionDidStart(_:)
  • Ошибка: extendedRuntimeSession(_:didInvalidateWith:error:)

Кроме того, когда сеанс останавливается (или мы завершаем его сами), мы не хотим, чтобы побочные эффекты, такие как showFailureToStartScreen(), возникали случайным образом. Мы хотим, чтобы они обрабатывались явно в другом месте кода. Следовательно, здесь выгодно иметь одноразовый конвейер, поэтому мы можем гарантировать, что sink вызывается только один раз.


Я понимаю, что один из способов сделать это - использовать Future, сохранить ссылку на Promise и вызвать обещание позже, но в лучшем случае это кажется хакерским:

class Manager: NSObject, WKExtendedRuntimeSessionDelegate {
  var session: WKExtendedRuntimeSession?
  var tempPromise: Future<Void, Error>.Promise?

  func startSession() -> AnyPublisher<Void, Error> {
    session = WKExtendedRuntimeSession()
    session?.delegate = self
    return Future { promise in
      tempPromise = promise
      session?.start()
    }.eraseToAnyPublisher()
  }

  func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
    tempPromise?(.success(()))
    tempPromise = nil
  }

  func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
    if let error = error {
      tempPromise?(.failure(error))
    }
    tempPromise = nil
  }
}

Это действительно самый элегантный способ работы с делегатами + одноразовые издатели, или есть более элегантный способ сделать это в Combine?


Для справки: PromiseKit также имеет API, аналогичный Future.init. А именно, pending() (пример):

  func startSession() -> Promise {
    let (promise, resolver) = Promise.pending()
    tempPromiseResolver = resolver

    session = WKExtendedRuntimeSession()
    session?.delegate = self
    session?.start()

    return promise
  }

person Senseful    schedule 19.11.2020    source источник


Ответы (1)


Вы можете обеспечить единовременного издателя с помощью оператора .first():

let subject = PassthroughSubject<Int, Never>()

let publisher = subject.first()

let c = publisher.sink(receiveCompletion: {
    print($0)
}, receiveValue: {
    print($0)
})

subject.send(1)
subject.send(2)

Результатом будет:

1
finished
person New Dev    schedule 20.11.2020