agentsclimarketplace

Combine expert

Skill almasumdev/awesome-ios-agent-skills/.github/skills/concurrency_and_networking/combine-expert

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-expert

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 Combine publishers, subjects, operators, backpressure, error handling, and how to decide when to migrate to async/await. Use for Combine-heavy codebases.

SKILL.md

5.3 KB, as published. Nobody here has run it

Combine Expert Patterns

Instructions

Combine is still the right tool when you're sitting on Apple's declarative SDK boundaries (@Published, NotificationCenter, NSObject KVO), or when your codebase hasn't yet migrated. Keep chains short, errors explicit, and subscriptions owned.

1. Publishers, Operators, Subscribers

import Combine

struct Search {
    let results: AnyPublisher<[Article], Error>

    static func make(query: AnyPublisher<String, Never>, api: API) -> Search {
        let pub = query
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .filter { $0.count >= 2 }
            .map { api.search($0) }
            .switchToLatest()
            .eraseToAnyPublisher()
        return Search(results: pub)
    }
}

2. Subjects

  • PassthroughSubject — emits only to current subscribers (events).
  • CurrentValueSubject — holds and emits current value (state).
  • @Published — property wrapper sugar for CurrentValueSubject semantics.
final class Cart: ObservableObject {
    @Published private(set) var items: [Item] = []
    func add(_ item: Item) { items.append(item) }
}

3. Error Handling

Combine requires Failure types to match across operators. Use mapError, catch, replaceError at the boundary you want to neutralize:

apiPublisher
    .retry(2)
    .map(Output.success)
    .catch { error -> Just<Output> in Just(.failure(error)) }
    .receive(on: DispatchQueue.main)
    .sink { output in self.handle(output) }
    .store(in: &cancellables)

Use tryMap to introduce errors, mapError to translate them to your domain type.

4. Schedulers

  • DispatchQueue.main for UI side effects.
  • DispatchQueue.global(qos: .userInitiated) for background transforms.
  • RunLoop.main for UIKit animations that need the .common mode.

Always receive(on:) before touching UI. Don't scatter .receive(on:) — put it as close to the sink as possible.

5. Backpressure

Combine has no formal backpressure, but operators control flow:

  • throttle(for:scheduler:latest:) — one event per window.
  • debounce(for:scheduler:) — one event after a quiet period.
  • collect(_:) — batch into arrays.
  • switchToLatest() — cancel in-flight when a new inner publisher starts (searches, typeahead).

6. Combining Streams

Publishers.CombineLatest3(user, settings, features)
    .map { UIState(user: $0.0, settings: $0.1, features: $0.2) }
    .receive(on: DispatchQueue.main)
    .assign(to: &$state)        // &$state requires @Published

zip pairs events one-to-one; combineLatest fires on any upstream emission; merge interleaves same-type streams.

7. Testing

Use a test scheduler (e.g., combine-schedulers from Point-Free) to drive time deterministically:

import CombineSchedulers

func test_debounceSearch() {
    let scheduler = DispatchQueue.test
    let input = PassthroughSubject<String, Never>()
    var outputs: [String] = []
    let c = input
        .debounce(for: .milliseconds(300), scheduler: scheduler)
        .sink { outputs.append($0) }
    input.send("a")
    scheduler.advance(by: .milliseconds(299))
    input.send("ab")
    scheduler.advance(by: .milliseconds(300))
    XCTAssertEqual(outputs, ["ab"])
    _ = c
}

8. Owning Subscriptions

Every sink / assign returns an AnyCancellable. Store it — otherwise the subscription cancels immediately:

private var cancellables = Set<AnyCancellable>()

somePublisher
    .sink { [weak self] in self?.handle($0) }
    .store(in: &cancellables)

In view models, clear cancellables on deinit or when restarting a pipeline.

9. Bridging to async/await

Use .values to consume any publisher as an AsyncSequence:

for try await article in apiPublisher.values {
    handle(article)
}

For a single value, use async extensions or a small helper:

extension Publisher {
    func firstValue() async throws -> Output {
        try await withCheckedThrowingContinuation { cont in
            var c: AnyCancellable?
            c = self.first()
                .sink(receiveCompletion: { if case .failure(let e) = $0 { cont.resume(throwing: e) }; _ = c },
                      receiveValue: { cont.resume(returning: $0); _ = c })
        }
    }
}

10. When to Migrate

Migrate when:

  • Operator chains exceed ~5 steps or nest flatMaps — async/await is clearer.
  • You need structured cancellation tied to view lifetime (use .task).
  • The team hits Swift 6 Sendable friction from reference-heavy publishers.

Keep Combine when:

  • You're consuming an Apple SDK that still vends publishers.
  • @Published + ObservableObject is already widespread and migration cost outweighs benefits.

Checklist

  • Every subscription is stored — no orphan pipelines.
  • receive(on:) is set immediately before UI sinks.
  • Error types are explicit; no .eraseToAnyPublisher() after untyped errors.
  • Tests use a virtual scheduler for anything time-based.
  • Migration plan exists for long-term move to async/await.

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.