I have an extension which takes a publisher and waits until a non-nil value is published before taking its value and returning it for use as an async/await function.
extension Publisher {
func value() async throws -> Output {
try await self
.compactMap { $0 }
.eraseToAnyPublisher()
.async()
}
}
enum AsyncError: Error {
case finishedWithoutValue
}
extension AnyPublisher {
/// Returns the first value of the publisher
@discardableResult
func async() async throws -> Output {
try await withCheckedThrowingContinuation { continuation in
var cancellable: AnyCancellable?
var finishedWithoutValue = true
cancellable = first()
.sink { result in
switch result {
case .finished:
if finishedWithoutValue {
continuation.resume(throwing: AsyncError.finishedWithoutValue)
}
case let .failure(error):
continuation.resume(throwing: error)
}
cancellable?.cancel()
} receiveValue: { value in
finishedWithoutValue = false
continuation.resume(with: .success(value))
}
}
}
}
For some reason, when I use it on an optional @Published
value, it returns an optional, rather than returning the unwrapped value of it. Since it waits until a non-nil value returns, why isn't it unwrapping it? For example, assuming foo
is an optional published value:
let one = await $foo.value() // Returns an optional
let two = await $foo.compactMap { $0 }.value() // Returns a non-optional
How do I fix this?
CodePudding user response:
The compactMap
with function that returns T?
will produce a publisher that emits type T
, so Publisher<T,Error>
. When you call value
on it directly the associated type Output
is T
so the compiler creates a version of value
that returns T
.
By construction the type of $foo
is a publisher whose associated type Output
is an optional or T?
, Publisher<T?,Error>
, so when you call value
on that the compiler constructs a function that returns T?
, an optional.
Inside of value
you use compactMap
to strip the optional part away, however, because you've said you want a return Output
, which isT?
, the compiler implicitly converts your T
to a T?
and returns it.
You can change your value
function so that it specializes for Optionals
:
func value<T>() async throws -> T where Output == Optional<T>{
try await self
.compactMap { $0 }
.eraseToAnyPublisher()
.async()
}
The problem is that value
will no longer work for non-optional types. You would need two functions... say value
and optValue
to handle the separate types.