swift-combine

Master Combine framework for reactive programming - publishers, subscribers, operators, schedulers

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swift-combine" with this command: npx skills add pluginagentmarketplace/custom-plugin-swift/pluginagentmarketplace-custom-plugin-swift-swift-combine

Swift Combine Skill

Reactive programming with Apple's Combine framework for handling asynchronous events and data streams.

Prerequisites

  • iOS 13+ / macOS 10.15+
  • Understanding of closures and generics
  • Familiarity with async programming concepts

Parameters

parameters:
  use_async_await:
    type: boolean
    default: true
    description: Prefer async/await where possible (iOS 15+)
  scheduler:
    type: string
    enum: [main, background, immediate]
    default: main
  error_handling:
    type: string
    enum: [catch, retry, replace]
    default: catch

Topics Covered

Core Concepts

ConceptDescription
PublisherEmits values over time
SubscriberReceives values from publisher
OperatorTransforms/filters values
SubjectPublisher + manual value injection
CancellableSubscription lifecycle

Common Publishers

PublisherPurpose
JustSingle value, then complete
FutureSingle async result
PassthroughSubjectManual value broadcast
CurrentValueSubjectManual + current value
@PublishedProperty wrapper publisher

Key Operators

CategoryOperators
Transformmap, flatMap, scan
Filterfilter, removeDuplicates, compactMap
Combinemerge, combineLatest, zip
Timingdebounce, throttle, delay
Errorcatch, retry, mapError

Code Examples

Basic Publisher Chain

import Combine

final class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published private(set) var results: [SearchResult] = []
    @Published private(set) var isLoading = false
    @Published private(set) var error: Error?

    private var cancellables = Set<AnyCancellable>()
    private let searchService: SearchService

    init(searchService: SearchService) {
        self.searchService = searchService
        setupSearch()
    }

    private func setupSearch() {
        $searchText
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main) // Wait for typing pause
            .removeDuplicates() // Don't search same query twice
            .filter { $0.count >= 2 } // Minimum characters
            .handleEvents(receiveOutput: { [weak self] _ in
                self?.isLoading = true
                self?.error = nil
            })
            .flatMap { [searchService] query -> AnyPublisher<[SearchResult], Never> in
                searchService.search(query: query)
                    .catch { error -> Just<[SearchResult]> in
                        // Handle error, return empty
                        return Just([])
                    }
                    .eraseToAnyPublisher()
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] results in
                self?.isLoading = false
                self?.results = results
            }
            .store(in: &cancellables)
    }
}

Subjects for Event Broadcasting

final class EventBus {
    static let shared = EventBus()

    // PassthroughSubject - no current value
    let userActions = PassthroughSubject<UserAction, Never>()

    // CurrentValueSubject - maintains current value
    let authState = CurrentValueSubject<AuthState, Never>(.loggedOut)

    private init() {}

    func send(_ action: UserAction) {
        userActions.send(action)
    }

    func login(user: User) {
        authState.send(.loggedIn(user))
    }

    func logout() {
        authState.send(.loggedOut)
    }
}

enum UserAction {
    case tappedButton(String)
    case viewedScreen(String)
    case completedPurchase(orderId: String)
}

enum AuthState {
    case loggedOut
    case loggedIn(User)
}

// Subscription
class AnalyticsService {
    private var cancellables = Set<AnyCancellable>()

    init() {
        EventBus.shared.userActions
            .sink { [weak self] action in
                self?.track(action)
            }
            .store(in: &cancellables)
    }

    private func track(_ action: UserAction) {
        // Send to analytics
    }
}

Combining Multiple Publishers

final class CheckoutViewModel: ObservableObject {
    @Published var cartItems: [CartItem] = []
    @Published var shippingAddress: Address?
    @Published var paymentMethod: PaymentMethod?
    @Published var promoCode: String = ""

    @Published private(set) var canCheckout = false
    @Published private(set) var total: Decimal = 0

    private var cancellables = Set<AnyCancellable>()

    init() {
        setupValidation()
        setupTotalCalculation()
    }

    private func setupValidation() {
        Publishers.CombineLatest3($cartItems, $shippingAddress, $paymentMethod)
            .map { items, address, payment in
                !items.isEmpty && address != nil && payment != nil
            }
            .assign(to: &$canCheckout)
    }

    private func setupTotalCalculation() {
        Publishers.CombineLatest($cartItems, $promoCode)
            .map { items, promo -> Decimal in
                let subtotal = items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
                let discount = self.calculateDiscount(promo: promo, subtotal: subtotal)
                return subtotal - discount
            }
            .assign(to: &$total)
    }

    private func calculateDiscount(promo: String, subtotal: Decimal) -> Decimal {
        // Promo code logic
        return 0
    }
}

Error Handling & Retry

extension Publisher {
    func retryWithBackoff(
        maxRetries: Int = 3,
        initialDelay: TimeInterval = 1,
        maxDelay: TimeInterval = 30
    ) -> AnyPublisher<Output, Failure> {
        self.catch { error -> AnyPublisher<Output, Failure> in
            guard maxRetries > 0 else {
                return Fail(error: error).eraseToAnyPublisher()
            }

            let delay = min(initialDelay * pow(2, Double(3 - maxRetries)), maxDelay)

            return Just(())
                .delay(for: .seconds(delay), scheduler: DispatchQueue.global())
                .flatMap { _ in
                    self.retryWithBackoff(
                        maxRetries: maxRetries - 1,
                        initialDelay: initialDelay,
                        maxDelay: maxDelay
                    )
                }
                .eraseToAnyPublisher()
        }
        .eraseToAnyPublisher()
    }
}

// Usage
apiClient.fetchData()
    .retryWithBackoff(maxRetries: 3)
    .catch { error -> Just<Data> in
        Logger.network.error("Final failure: \(error)")
        return Just(Data())
    }
    .sink { data in
        // Process data
    }
    .store(in: &cancellables)

Bridging to async/await

extension Publisher {
    func firstValue() async throws -> Output {
        try await withCheckedThrowingContinuation { continuation in
            var cancellable: AnyCancellable?

            cancellable = self.first()
                .sink(
                    receiveCompletion: { completion in
                        switch completion {
                        case .finished:
                            break
                        case .failure(let error):
                            continuation.resume(throwing: error)
                        }
                        cancellable?.cancel()
                    },
                    receiveValue: { value in
                        continuation.resume(returning: value)
                    }
                )
        }
    }
}

// Usage
let result = try await somePublisher.firstValue()

Troubleshooting

Common Issues

IssueCauseSolution
Sink never calledNo strong referenceStore in cancellables Set
UI not updatingWrong schedulerUse .receive(on: DispatchQueue.main)
Memory leakStrong reference in closureUse [weak self]
Duplicate eventsMissing removeDuplicatesAdd .removeDuplicates()
Publisher completes earlyUsing first() or prefix()Check operator semantics

Debug Tips

// Print debug info for each event
publisher
    .print("Debug")
    .sink { ... }

// Handle events at each stage
publisher
    .handleEvents(
        receiveSubscription: { _ in print("Subscribed") },
        receiveOutput: { print("Output: \($0)") },
        receiveCompletion: { print("Completed: \($0)") },
        receiveCancel: { print("Cancelled") }
    )
    .sink { ... }

// Breakpoint on specific conditions
publisher
    .breakpoint(receiveOutput: { $0 > 100 })
    .sink { ... }

Validation Rules

validation:
  - rule: store_cancellables
    severity: error
    check: All subscriptions must be stored in cancellables
  - rule: weak_self_in_closures
    severity: warning
    check: Use [weak self] in sink closures
  - rule: receive_on_main
    severity: warning
    check: UI updates must receive on main queue

Usage

Skill("swift-combine")

Related Skills

  • swift-swiftui - @Published integration
  • swift-concurrency - async/await alternative
  • swift-networking - Network publishers

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

swift-uikit

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

swift-macos

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

swift-architecture

No summary provided by upstream source.

Repository SourceNeeds Review