Я не нашел более элегантного способа сделать №2.
Вот лучшее решение №3 (Void -> Never
, завершить немедленно):
let voidPublisher: AnyPublisher<Void, Never> = ...
let neverPublisher: AnyPublisher<Never, Never> = Publishers.Merge(Just<Void>(()), saveToDatabase)
.first()
.ignoreOutput()
.eraseToAnyPublisher()
А вот игровая площадка для синхронизации Я использовал, чтобы убедиться, что все работает должным образом:
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")
Наконец, вот решения в качестве операторов на 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()
}
}
}