I’m not here to debate whether Combine is “dead.” Personally, I believe Swift Concurrency and Combine both continue to serve real-world needs - one leaning imperative, the other functional-reactive.
I recently revisited Michael Long’s post, which highlights where Swift Concurrency and Combine overlap and diverge, especially around back-pressure management.
What I’m curious about is how these two async frameworks can play together more cleanly.
Since Combine hasn’t received much attention in recent years, it lacks the appropriate interpolation points to fit smoothly with Concurrency patterns. A key example is scheduling Combine subscriptions on actors. If you mark an ObservableObject as @MainActor and set up a Combine pipeline without explicitly receive(on:) the main queue, you can easily hit a runtime 'swift_task_checkIsolated' crash when updating properties.
There is no compiler warning here - just a runtime crash - highlighting a painful gap in interop.
This does not automatically fix actor-isolation issues.
Inside a @Sendable closure, you do not have @MainActor isolation automatically , so you must explicitly hop back to the main actor (e.g., using await MainActor.run { ... } or Task { @MainActor in ... } ) to safely update @MainActor -isolated properties.
It also requires ensuring your captured data is Sendable, forcing additional type audits.
In practice, it feels like an awkward gap when mixing these frameworks in SwiftUI apps, requiring extra care to avoid subtle concurrency bugs.
Another example of interop friction is combining async operations inside a Combine pipeline. We’ve worked around this by defining an asyncMap extension that allows us to await within the pipeline while continuing downstream:
Is there official guidance or roadmap intent around improving Combine + Swift Concurrency interop, particularly with actor isolation?
Would it be possible for the compiler to detect and warn about Combine pipelines updating @MainActor-isolated properties from the wrong context?
Are there idiomatic patterns or libraries the community has found helpful for reducing this friction?
Should we consider additional language or framework features (like Publisher.receive(on actor:)) to ease this interop pain?
I would love to hear others’ experiences, best practices, or insights on mixing Combine with Swift Concurrency safely and ergonomically in modern SwiftUI apps.
Combine is private Apple framework and isn’t related to Swift language, on the topic you’d better ask on Apple forums or file a radar with issues.
To my understanding, there is no plans from Apple side to evolve Combine further. They haven’t accommodated many things to new concurrency for the last few years in it, and actively replacing its usage with new features like Observable macro.
Also, my experience with reactive style of Combine and new concurrency tells that they pair poorly: thry are two too distant concepts, that work to solve similar problem in a different ways, making them hard to align.
Combine is private Apple framework and isn’t related to Swift language
While this is true, reactive programming and Combine in particular has been a widely used paradigm in many projects. Due to its downfalls and the upcoming advancements in Swift Concurrency many teams are switching away from it.
Now there are a couple of recurring scenarios where adapters between publishers and async code are needed during this migration phase. Apple, for example, provides the values property to convert a publisher to an async sequence.
I think it totally makes sense to discuss these and other migration tools on the Swift forum, as they do affect how to use the new language features together with old code.
Combine isn't "fixable". You could go through and add : Sendable requirements to all the Output associated types, and @Sendable to all the closures, and it'd be more right, but it'd error on lots of valid Combine code, such as
@MainActor
final class MyViewModel {
@Published var value = 3
func whatever() {
let cancellable = Just(42)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.value = $0 // assigning mainactor-isolated property from @Sendable closure
}
}
}
and still fail to error on lots of invalid code such as
@MainActor
final class MyViewModel {
@Published var value = 3
func whatever() {
let cancellable = Just(42)
.receive(on: DispatchQueue.global)
.assign(to: &$myMainActorPublishedProperty) // oops, assigning mainactor-isolated property from global queue
}
}
(And no, receive(on:) is very far from the only problem; it's just an easy one to pick on. Buffer (which delivers some values in its upstream's isolation, and others in its downstream's isolation), is particularly twisted)
I do wonder whether somebody should fork OpenCombine and make a best effort to make it safe whilst breaking as few APIs as possible, but I'm not sure that real projects could realistically replace actual existing Combine code with the result. And I definitely don't think new projects should pick it up.
Nevertheless there are tons of reactive code out there that can't be refactored instantaneously and that still has to be worked with. I find it helpful to discuss the interface between Combine and Swift Concurrency.
How can Publisher.values be used in a safe way? What has to be done to design a safer alternative? Under which circumstances is code correct, and how can we convince Swift 6 language mode? Are these topics for the migration guide?
It can't; it has no internal buffer, and Combine doesn't reliably respect backpressure. You can put a Combine buffer in front of it: publisher.buffer(...).values, which is nearly OK, but Buffer is also broken in strange and subtle ways (most notably that it delivers completion instantaneously, regardless of how many values are still buffered up).
Swift provides Observable and AsyncSequence, which together cover the vast majority of Combine's use-cases. (one for push, one for pull). Of course, since Combine operated an inconsistent hybrid of push and pull, plenty of Combine code can't be directly translated to either paradigm.
Under the same circumstances as it was previously, which is to say, rarely...
You absolutely cannot reconcile Combine's API with Swift 6's safety restrictions (as the above examples show). You might be able to create a new API in the spirit of Combine which is also Swift 6-compatible (You could start by refactoring OpenCombine, even). But it would not end up anywhere near API-compatible with Combine.
The Swift migration guide is about Swift, not about Apple's frameworks. This is either Apple's problem (and since Combine has been abandoned for multiple years now, I think it's fair to say they are not likely to take an interest), or a community concern.