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.
