I'm wrapping async requests in Combine publishers so they can easily be used across different pipelines.
A consumer might hold on to these publishers as follows:
struct Dependencies {
var loadImageRequest: AnyPublisher<UIImage, Never>
var saveToDatabaseRequest: AnyPublisher<Void, Never>
var saveToUserDefaultsRequest: AnyPublisher<Never, Never>
}
Two of the more common types of requests are:
AnyPublisher<Never, Never>
is a good way to express this type. This can easily be constructed via Empty<Never, Never>(completeImmediately: true)
.AnyPublisher<Void, Never>
to model these request types. An easy way to construct these is via a Future<Void, Never>() { promise in promise(.success(()))}
.Both of these have the common theme of ignoring the result. Therefore, when handing these off to the consumers, it's sometimes useful to convert between these two data types: AnyPublisher<Never, Never>
and AnyPublisher<Void, Never>
.
There are three potential ways to convert between the two:
Never -> Void
, complete immediately
One way to convert this is with some forced casting:
let neverPublisher: AnyPublisher<Never, Never> = ...
let voidPublisher: AnyPublisher<Void, Never> = neverPublisher
.map { _ in () }
.append(Just(()))
.eraseToAnyPublisher()
Void -> Never
, wait until Void completes
Since there is a bulit-in operator for this one, the conversion is easy:
let voidPublisher: AnyPublisher<Void, Never> = ...
let neverPublisher: AnyPublisher<Never, Never> = voidPublisher
.ignoreOutput()
.eraseToAnyPublisher()
Void -> Never
, complete immediately
I'm not sure the best way to do this conversion. The solution I came up with has two major downsides: using handleEvents
, and needing cancellables
defined somewhere:
let cancellables: Set<AnyCancellable> = []
let voidPublisher: AnyPublisher<Void, Never> = ...
let neverPublisher: AnyPublisher<Never, Never> = Empty<Never, Never>(completeImmediately: true)
.handleEvents(receiveCompletion: { _ in voidPublisher.sink(receiveValue: { _ in }).store(in: &cancellables) })
.eraseToAnyPublisher()
Questions:
Never -> Void
, complete immediately) without needing to invoke both map
and append
? (E.g. similar to how ignoreOutput
was used to solve the second conversion?)Void -> Never
, complete immediately) which doesn't need cancellables
?I haven't found a more elegant way to do #2.
Here is a better #3 (Void -> Never
, complete immediately) solution:
let voidPublisher: AnyPublisher<Void, Never> = ...
let neverPublisher: AnyPublisher<Never, Never> = Publishers.Merge(Just<Void>(()), saveToDatabase)
.first()
.ignoreOutput()
.eraseToAnyPublisher()
And here is the timing Playground I used to ensure everything is functioning as expected:
import Combine
import Foundation
let delaySec: TimeInterval = 0.1
let halfDelaySec: TimeInterval = delaySec / 2
let halfDelayMicroSeconds: useconds_t = useconds_t(halfDelaySec * 1_000_000)
let sleepBufferMicroSeconds: useconds_t = useconds_t(0.01 * 1_000_000)
var cancellables = [AnyCancellable]()
var output: [String] = []
func performVoidAction(voidPublisher: AnyPublisher<Void, Never>) {
voidPublisher
.handleEvents(
receiveCompletion: { _ in output.append("performVoidAction - completion") },
receiveCancel: { output.append("performVoidAction - cancel") })
.sink(receiveValue: { output.append("performVoidAction - sink") })
.store(in: &cancellables)
}
func performNeverAction(neverPublisher: AnyPublisher<Never, Never>) {
neverPublisher
.handleEvents(
receiveCompletion: { _ in output.append("performNeverAction - completion") },
receiveCancel: { output.append("performNeverAction - cancel") })
.sink(receiveValue: { _ in output.append("performNeverAction - sink") })
.store(in: &cancellables)
}
func makeSaveToDatabasePublisher() -> AnyPublisher<Void, Never> {
Deferred { _saveToDatabase() }.eraseToAnyPublisher()
}
func makeSaveToUserDefaultsPublisher() -> AnyPublisher<Never, Never> {
Deferred { _saveToUserDefaults() }.eraseToAnyPublisher()
}
// --->(Void)|
// AnyPublisher<Void, Never> wraps an API that does something and finishes upon completion.
private func _saveToDatabase() -> AnyPublisher<Void, Never> {
return Future<Void, Never> { promise in
output.append("saving to database")
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: DispatchTime.now() + delaySec) {
output.append("saved to database")
promise(.success(()))
}
}.eraseToAnyPublisher()
}
// |
// AnyPublisher<Never, Never> wraps an API that does something and completes immediately, it does not wait for completion.
private func _saveToUserDefaults() -> AnyPublisher<Never, Never> {
output.append("saved to user defaults")
return Empty<Never, Never>(completeImmediately: true)
.eraseToAnyPublisher()
}
func assert(_ value: Bool) -> String {
value ? "✅" : "❌"
}
// tests
assert(output.isEmpty)
var saveToDatabase = makeSaveToDatabasePublisher()
assert(output.isEmpty, "It should not fire the action yet.")
// verify database save, first time
performVoidAction(voidPublisher: saveToDatabase)
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty)
// verify database save, second time
performVoidAction(voidPublisher: saveToDatabase)
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty)
var saveToUserDefaults = makeSaveToUserDefaultsPublisher()
assert(output.isEmpty, "It should not fire the action yet.")
// verify user defaults save, first time
performNeverAction(neverPublisher: saveToUserDefaults)
assert(!output.isEmpty && output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output
// verify user defaults save, second time
performNeverAction(neverPublisher: saveToUserDefaults)
assert(!output.isEmpty && output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output
// MARK: - Problem: AnyPublisher<Never, Never> -> AnyPublisher<Void, Never>
// MARK: Solution 1
// `|` ➡️ `(Void)|`
performVoidAction(
voidPublisher: saveToUserDefaults
.map { _ in () }
.append(Just(()))
.eraseToAnyPublisher())
assert(output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output"
// MARK: - Problem: AnyPublisher<Void, Never> -> AnyPublisher<Never, Never>
// MARK: Solution 2 (Wait)
// `--->(Void)|` ➡️ `--->|`
performNeverAction(
neverPublisher: saveToDatabase.ignoreOutput().eraseToAnyPublisher())
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty)
// MARK: Solution 3 (No wait)
// `--->(Void)|` ➡️ `|`
performNeverAction(
neverPublisher: Publishers.Merge(Just<Void>(()), saveToDatabase)
.first()
.ignoreOutput()
.eraseToAnyPublisher())
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(output.isEmpty)
print("done")
Finally, here are the solutions as operators on Publisher:
extension Publisher where Output == Never {
func asVoid() -> AnyPublisher<Void, Failure> {
self
.map { _ in () }
.append(Just(()).setFailureType(to: Failure.self))
.eraseToAnyPublisher()
}
}
extension Publisher where Output == Void {
func asNever(completeImmediately: Bool) -> AnyPublisher<Never, Failure> {
if completeImmediately {
return Just<Void>(())
.setFailureType(to: Failure.self)
.merge(with: self)
.first()
.ignoreOutput()
.eraseToAnyPublisher()
} else {
return self
.ignoreOutput()
.eraseToAnyPublisher()
}
}
}
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