How do you move between Combine and async/await: the values property, Future around a Task, and what to watch for?
Predict first
What do these two bridges return?
import Combine
import Foundation
let first = await (1...5).publisher.values.first { $0 % 2 == 0 }
print("first even through async:", first ?? -1)
func answer() -> Future<Int, Never> {
Future { promise in Task { promise(.success(7)) } }
}
var bag = Set<AnyCancellable>()
let value = await withCheckedContinuation { (c: CheckedContinuation<Int, Never>) in
answer().sink { c.resume(returning: $0) }.store(in: &bag)
}
print("Future wrapping async work:", value)
- first even through async: 2 / Future wrapping async work: 7
- first even through async: 1 / Future wrapping async work: 7
- It hangs
Mental model. values is a translator at the border who turns radio broadcasts into a book you can read at your own pace. Future-around-Task goes the other way: a letter-writer who dictates an async answer back to the radio.
Combine to async: every publisher with Failure == Never has a .values property that is an AsyncSequence (an AsyncPublisher; AsyncThrowingPublisher for failing ones), so for await works, and so do algorithms like .first { }: the check gets 2 as the first even number from (1...5).publisher. Subscription happens when iteration starts, demand is requested a value at a time, and cancelling the task cancels the subscription. This is the clean way to consume a publisher from inside an async function or a .task modifier.
async to Combine: wrap the work in Future { promise in Task { … promise(.success(x)) } } and expose it as a publisher (the check returns 7). Keep the wrapper tiny, remember the Future is eager and not cancellable (use Deferred if you want laziness, Q536 earlier in this section), and call promise exactly once. Things to watch: a subject-backed .values loop only receives values sent after the loop has subscribed (so start consuming before producing), mixing the two styles across a codebase hides the thread and isolation story (publishers deliver on whatever scheduler you chose, async code on actors), and strict concurrency may require the values to be Sendable to cross the boundary. For new code prefer one model per feature and bridge at the edges.
values gives async access to a publisher; a Future can wrap a Task and resume once.
Common traps
- Starting to read
.valuesafter the subject already sent. Earlier events are not replayed. - Wrapping async work in a
Futureand expecting laziness or cancellation. It starts at creation and cannot be cancelled.
They might follow up with
- Does
.valuesapply backpressure? Yes; it requests one value at a time, so a slow consumer slows a demand-respecting source (Q303). - Which side should own the lifetime? The async side: tie the loop to a task that is cancelled with the screen.
Related questions
- 11.8.1 When does a Future run its work, what do two subscribers get, and what does Deferred change?
- 11.8.3 What does share() do for several subscribers, and what happens to a subscriber who arrives after the shared…
- 10.6.2 How do you bridge repeated events, GCD and Combine into async code with AsyncStream?
- 11.6.1 When do you use Combine, AsyncSequence or the Observation framework, and when should you not use Combine?
← What does share() do for several subscribers, and what happens to a…