agentsclimarketplace

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.

Install
npx -y skills add almasumdev/awesome-ios-agent-skills --skill combine-to-async-await

Assembled 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

Combineasync/await equivalent
Future<Output, Error>async throws -> Output
AnyPublisher<Output, Error>AsyncThrowingStream<Output, Error>
AnyPublisher<Output, Never>AsyncStream<Output>
PassthroughSubjectAsyncStream.makeStream() continuation
CurrentValueSubject@Observable property + stream
sink { } subscriptionfor await value in sequence { }
.map, .filter.map, .filter on AsyncSequence
.combineLatest, .zipasync let pairs, withTaskGroup
.flatMapnested await
Cancellation via AnyCancellableTask 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

  • @Published is still convenient inside legacy ObservableObject models — keep it until you migrate to @Observable.
  • Apple SDKs (e.g., some StoreKit, CoreLocation adapters) still return publishers. Convert with .values at the call site rather than rewriting upstream.
  • Complex operator chains on finite sources — migrate deliberately, not for its own sake.

9. Migration Steps

  1. Identify publishers with a single value → rewrite as async throws.
  2. Identify long-running publishers → expose as AsyncSequence.
  3. Replace sink sites with for await loops inside Tasks.
  4. Kill Set<AnyCancellable> storage; move to structured Task ownership.
  5. Delete Combine imports when no references remain.

Checklist

  • New APIs return async throws or AsyncSequence, not publishers.
  • Callback APIs are bridged with withCheckedContinuation, cancellation handled.
  • No Set<AnyCancellable> fields in new view models.
  • combineLatest is replaced with async let or TaskGroup.
  • Remaining Combine use is scoped to SDK boundaries and documented.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.