Combine to async await
Skill almasumdev/awesome-ios-agent-skills/.github/skills/migration/combine-to-async-await
Curated agent skills, conventions, and workflows for building iOS apps (Swift, SwiftUI, UIKit) with AI coding agents.
npx -y skills add almasumdev/awesome-ios-agent-skills --skill combine-to-async-awaitAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Expert guidance on migrating Combine publishers to Swift structured concurrency (async/await, AsyncSequence). Use when replacing Combine or adapting third-party APIs.
SKILL.md
5.5 KB, as published. Nobody here has run it
Combine → async/await Migration
Instructions
Swift structured concurrency is now the preferred concurrency model. Combine remains useful at framework boundaries (@Published, NotificationCenter.publisher, Apple SDKs that still return publishers), but new code should favor async/await and AsyncSequence.
1. Mental Mapping
| Combine | async/await equivalent |
|---|---|
Future<Output, Error> | async throws -> Output |
AnyPublisher<Output, Error> | AsyncThrowingStream<Output, Error> |
AnyPublisher<Output, Never> | AsyncStream<Output> |
PassthroughSubject | AsyncStream.makeStream() continuation |
CurrentValueSubject | @Observable property + stream |
sink { } subscription | for await value in sequence { } |
.map, .filter | .map, .filter on AsyncSequence |
.combineLatest, .zip | async let pairs, withTaskGroup |
.flatMap | nested await |
Cancellation via AnyCancellable | Task cancellation + Task.isCancelled |
2. Single-Value Publishers → async throws
Before:
func fetchUser(id: String) -> AnyPublisher<User, Error> {
URLSession.shared.dataTaskPublisher(for: url(id))
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
After:
func fetchUser(id: String) async throws -> User {
let (data, response) = try await URLSession.shared.data(from: url(id))
try HTTPError.ensureOK(response)
return try JSONDecoder().decode(User.self, from: data)
}
3. Stream Publishers → AsyncSequence
Use .values on any Publisher:
for try await article in articlesPublisher.values {
handle(article)
}
For NotificationCenter:
for await note in NotificationCenter.default.notifications(named: UIApplication.didBecomeActiveNotification) {
refresh()
}
4. Adapting Callback APIs
Bridge with withCheckedThrowingContinuation:
func loadImage(named name: String) async throws -> UIImage {
try await withCheckedThrowingContinuation { cont in
ImageLoader.shared.load(name) { result in
switch result {
case .success(let image): cont.resume(returning: image)
case .failure(let error): cont.resume(throwing: error)
}
}
}
}
Pair with withTaskCancellationHandler for cancellable APIs:
func download(_ url: URL) async throws -> Data {
let handle = Downloader()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { cont in
handle.start(url) { cont.resume(with: $0) }
}
} onCancel: {
handle.cancel()
}
}
5. Replacing Subjects
PassthroughSubject:
let (stream, continuation) = AsyncStream<Event>.makeStream()
// emit:
continuation.yield(.tapped)
// consume:
for await event in stream { handle(event) }
// finish:
continuation.finish()
CurrentValueSubject → an @Observable property plus an AsyncStream for external observers.
6. Replacing combineLatest / zip
async let user = fetchUser(id: id)
async let settings = fetchSettings(id: id)
let (u, s) = try await (user, settings)
Or with a TaskGroup for a dynamic set of requests:
try await withThrowingTaskGroup(of: Article.self) { group in
for id in ids { group.addTask { try await fetchArticle(id: id) } }
var out: [Article] = []
for try await article in group { out.append(article) }
return out
}
7. Cancellation
Combine cancels when the subscription is released. async/await cancels when the owning Task cancels. In SwiftUI, attach work to a view with .task:
.task(id: userID) {
do { try await model.load(userID) }
catch is CancellationError { /* ignore */ }
catch { model.error = error }
}
8. When to Keep Combine
@Publishedis still convenient inside legacyObservableObjectmodels — keep it until you migrate to@Observable.- Apple SDKs (e.g., some
StoreKit,CoreLocationadapters) still return publishers. Convert with.valuesat the call site rather than rewriting upstream. - Complex operator chains on finite sources — migrate deliberately, not for its own sake.
9. Migration Steps
- Identify publishers with a single value → rewrite as
async throws. - Identify long-running publishers → expose as
AsyncSequence. - Replace
sinksites withfor awaitloops insideTasks. - Kill
Set<AnyCancellable>storage; move to structuredTaskownership. - Delete Combine imports when no references remain.
Checklist
- New APIs return
async throwsorAsyncSequence, not publishers. - Callback APIs are bridged with
withCheckedContinuation, cancellation handled. - No
Set<AnyCancellable>fields in new view models. -
combineLatestis replaced withasync letorTaskGroup. - Remaining Combine use is scoped to SDK boundaries and documented.