Normally we can bridge our async code and Combine by wrapping our async code in a single-shot publisher using a 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()
}
On the other hand, if we're wrapping the delegate pattern (instead of an async callback), it's recommended to use a PassthroughSubject, since the methods could be fired multiple times:
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)
}
}
However, I'm trying to create a one-shot publisher which wraps an existing Delegate pattern. The reason is that I'm firing off a method like connect() and I expect either a success or failure to happen immediately. I do not want future updates to affect the pipeline.
For example, imagine I'm using WKExtendedRuntimeSession and wrapped the .start() method in startSession() below. If I have this wrapped successfully, I should be able to use it like so:
manager.startSession()
.sink(
receiveCompletion: { result in
if result.isError {
showFailureToStartScreen()
}
},
receiveValue: { value in
showStartedSessionScreen()
})
.store(in: &cancellables)
The reason a one-shot publisher is useful is because we expect one of the following two methods to be called soon after calling the method:
extendedRuntimeSessionDidStart(_:)extendedRuntimeSession(_:didInvalidateWith:error:)Furthermore, when the session is halted (or we terminate it ourselves), we don't want side effects such as showFailureToStartScreen() to randomly happen. We want them to be handled explicitly elsewhere in the code. Therefore, having a one-shot pipeline is beneficial here so we can guarantee that sink is only called once.
I realize that one way to do this is to use a Future, store a reference to the Promise, and call the promise at a later time, but this seems hacky at best:
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
}
}
Is this really the most elegant way to work with delegates + one-shot publishers, or is there a more elegant way to do this in Combine?
For reference, PromiseKit also has a similar API to Future.init. Namely, Promise.init(resolver:). However, PromiseKit also seems to natively support the functionality I describe above with their pending() function (example):
func startSession() -> Promise {
let (promise, resolver) = Promise.pending()
tempPromiseResolver = resolver
session = WKExtendedRuntimeSession()
session?.delegate = self
session?.start()
return promise
}
You can ensure a one-shot publisher with .first() operator:
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)
The output would be:
1
finished
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With