Обычно мы можем связать наш асинхронный код и объединить, заключив наш асинхронный код в одноразовый издатель, используя 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
}