Combine — key concepts, Publisher, and reactive programming

Author: IT Sectr Published: 2026-05-03 Reading time: 11 min

Combine is Apple's native reactive programming framework, introduced in iOS 13, macOS Catalina, tvOS 13, and watchOS 6. It provides a declarative Swift API for handling asynchronous events through the Publisher and Subscriber pattern, replacing delegates, closures, and NotificationCenter with a unified chain. According to Apple, 2025, Combine is the foundation for SwiftUI and modern iOS architectures, working closely with async/await and Structured Concurrency. The framework is designed for composing asynchronous operations with thread-safety guarantees.

Key Takeaways

  • Combine is Apple's native reactive framework with Publisher, Subscriber, Subject types and error types through Failure
  • Publisher emits events over time: Output values, can complete with success or Failure error
  • Subscriber receives events from Publisher and can request a specific number of items through Demand
  • Operators map, flatMap, filter, combineLatest, zip, debounce transform streams without race conditions
  • SwiftUI uses Combine through @Published, ObservableObject, and @StateObject for reactive interface updates

What is Combine?

Combine is a declarative reactive programming framework built into the Apple SDK. It implements the Reactive Streams pattern: Publisher produces values, Subscriber consumes them, and operators transform the stream between them. Combine solves the problem of callbacks and delegates by providing a unified composition model for any asynchronous events — from network responses to UI changes.

Before Combine, iOS developers used third-party libraries, primarily RxSwift. Apple created Combine as a native alternative with deep ecosystem integration: the framework supports Objective-C through @objc bridges, works with KVO (Key-Value Observing via NSObject.keyValuePublisher) and NotificationCenter, and also serves as the foundation for SwiftUI. All UIKit components published in SwiftUI use Combine under the hood for view updates.

Combine is designed with Swift Concurrency in mind: starting with iOS 15, a Publisher can be converted to AsyncSequence through .values and used in for-await-in loops. The reverse conversion of async functions to Publisher is done through Future. According to Apple WWDC 2024, Combine remains the recommended framework for handling streaming data in UIKit applications, despite the introduction of async/await for one-off asynchronous calls.

Core Combine Concepts

Combine is based on three protocols: Publisher (emits values of type Output, can fail with an error of type Failure), Subscriber (receives values, manages Demand — the number of requested elements), Subscription (represents the Publisher-Subscriber connection with cancel capability). The data channel is initialized on subscribe and ends on cancel, completion, or error. Demand is a unique Combine concept: Subscriber tells Publisher how many elements it is ready to process, implementing backpressure at the protocol level.

Publisher and Subscriber: Reactive Stream Architecture

Publisher is a protocol with two associated types: Output (the type of emitted values) and Failure (the error type conforming to Error). If the stream cannot fail, Failure is specified as Never — this guarantees the Subscriber that onReceive will only be called with Output. Built-in Publishers include Just (single value), Sequence (array), URLSession.DataTaskPublisher (network request), NotificationCenter.Publisher, and the @Published property wrapper.

swift
import Combine

// Creating Publisher from a sequence
let publisher = [1, 2, 3, 4, 5].publisher

// Creating Subscriber with value handling
class PrintSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never

    func receive(subscription: Subscription) {
        subscription.request(.unlimited)
    }

    func receive(_ input: Int) -> Subscribers.Demand {
        print("Received: \(input)")
        return .unlimited
    }

    func receive(completion: Subscribers.Completion<Never>) {
        print("Completed")
    }
}

publisher.subscribe(PrintSubscriber())

Subscription and Demand

Subscription is a protocol representing the active connection between Publisher and Subscriber. The Subscriber receives Subscription in the receive(subscription:) method and calls request(_:) to specify Demand: .unlimited (all values), .max(N) (limited count), or .none (pause). Demand can change dynamically — Subscriber can increase or decrease the number of requested elements during data reception. This provides backpressure without buffering on the Publisher side.

Subject and CurrentValueSubject

Subject is a type that combines Publisher and Subscriber. A Subject can be used as a Publisher (subscribers subscribe to it) and simultaneously as a Subscriber (values are sent into it). Combine provides two types of Subject: PassthroughSubject (does not store state, passes only new values) and CurrentValueSubject (stores the current value and passes it to new subscribers). Subject is necessary for integrating imperative code into Combine reactive chains.

swift
let subject = PassthroughSubject<String, Never>()

// Subscribing as a Publisher
let cancellable = subject
    .map { $0.uppercased() }
    .sink { print($0) }

// Sending values as a Subscriber
subject.send("hello")  // Prints "HELLO"
subject.send("world")  // Prints "WORLD"

CurrentValueSubject differs from PassthroughSubject by having an initial value and a value property: a subscriber immediately receives the current value upon subscription, and then all subsequent updates. CurrentValueSubject.value is readable and writable — changing value automatically sends the new value to all subscribers. This makes CurrentValueSubject an ideal choice for representing state in the MVVM architecture: ViewModel publishes CurrentValueSubject, View subscribes with changes via sink.

Both Subjects can terminate the stream by calling send(completion: .finished) or send(completion: .failure(error)). After termination, the Subject stops accepting and emitting events. For long-lived streams that should not terminate (e.g., UI events), it is recommended to use PassthroughSubject with Never Failure to avoid accidentally calling send(completion:).

Transformation and Combining Operators

Combine Operators are Publisher methods that return a new Publisher. Each operator creates a new object that subscribes to the upstream Publisher and emits transformed values downstream. Since Publisher is a generic type, operators maintain strict typing: map transforms Output<A> to Output<B>, tryMap adds the possibility of error. Combine contains about 100 built-in operators.

CategoryOperatorPurpose
Transformationmap / tryMap / flatMapTransform values or streams
Filteringfilter / compactMap / removeDuplicatesSelect or clean values
CombiningcombineLatest / zip / mergeCombine multiple Publishers
Time Controldebounce / throttle / delayDelay and throttle events
Error Handlingcatch / retry / replaceErrorRecovery after Failure
Demand Managementbuffer / collectGrouping or buffering

flatMap in Combine has an important difference from the RxSwift version: it accepts a closure returning a Publisher with the same Failure type and flattens the nested Publisher into the main stream. flatMap with maxPublishers: .max(1) behaves like switchMap — it cancels the previous nested Publisher when a new value arrives. This is critical for search scenarios: when a new character is typed, the previous HTTP request is automatically cancelled.

swift
// Debounce search with cancellation of previous request
searchTextField.textPublisher
    .debounce(for: .seconds(0.3), scheduler: RunLoop.main)
    .removeDuplicates()
    .flatMap(maxPublishers: .max(1)) { query in
        apiService.searchPublisher(query)
            .catch { _ in Just([]) }
    }
    .receive(on: DispatchQueue.main)
    .sink { results in
        self.tableView.reloadData()
    }
    .store(in: &cancellables)

Combining operators — combineLatest and zip — work similarly to RxSwift: combineLatest emits a tuple of the latest values from all Publishers when any of them changes; zip pairs values by index. merge combines Publishers of the same type into one stream, order preservation is not guaranteed. Combine also has select — a rare operator that picks the first completing Publisher among several, and share — multicasting a stream to multiple subscribers without re-execution.

Schedulers and Thread Management

Scheduler in Combine is a protocol that defines the execution context for operators. Unlike RxSwift with its 5+ built-in Schedulers, Combine uses existing Apple mechanisms: DispatchQueue, RunLoop, and OperationQueue. Each of these types conforms to the Scheduler protocol, allowing them to be passed directly to receive(on:) and subscribe(on:) without additional adapters.

receive(on:) switches downstream to the specified Scheduler — equivalent to observeOn in RxSwift. All operators after receive(on:) execute on the specified Scheduler. subscribe(on:) switches upstream — affects Publisher execution. A typical pattern: subscribe(on: DispatchQueue.global()) for background work and receive(on: DispatchQueue.main) for UI updates. In SwiftUI, when using .onReceive, built-in binding to the main thread is not required, but for sink, explicit receive(on:).main is recommended.

swift
// Background loading + UI on the main thread
URLSession.shared.dataTaskPublisher(for: url)
    .subscribe(on: DispatchQueue.global(qos: .background))
    .tryMap { data, response -> Data in
        guard let http = response as? HTTPURLResponse,
              http.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        return data
    }
    .receive(on: DispatchQueue.main)
    .decode(type: User.self, decoder: JSONDecoder())
    .sink(receiveCompletion: { print($0) },
         receiveValue: { self.nameLabel.text = $0.name })
    .store(in: &cancellables)

RunLoop.main is an alternative to DispatchQueue.main for UI operations. The difference is that RunLoop.main is tied to the current application event loop, while DispatchQueue.main is tied to the global main thread queue. For UIKit, DispatchQueue.main is recommended; for SwiftUI — RunLoop.main. ImmediateWhenScheduler executes operations synchronously on the current thread — used by default for tests and simple Publishers.

Combine and SwiftUI: Integration via ObservableObject

ObservableObject is a SwiftUI protocol for objects that publish changes. A class implementing ObservableObject can use the @Published property wrapper for properties whose changes automatically notify SwiftUI about the need to redraw. Under the hood, @Published creates a Publisher that notifies the objectWillChange Publisher when wrappedValue changes. SwiftUI subscribes to objectWillChange via @StateObject, @ObservedObject, or @EnvironmentObject.

@Published and @StateObject

@Published is the most common way to integrate Combine into SwiftUI. When a @Published property value changes, SwiftUI updates all Views using that object. @StateObject creates an ObservableObject instance and subscribes to its changes. A View created with @StateObject automatically redraws when @Published properties change. If the object needs to be shared across multiple Views, @ObservedObject or @EnvironmentObject is used.

swift
class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    private var cancellables = Set<AnyCancellable>()

    init() {
        $name
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .sink { [weak self] newName in
                AnalyticsService.logNameChange(newName)
            }
            .store(in: &cancellables)
    }
}

struct UserView: View {
    @StateObject var viewModel = UserViewModel()

    var body: some View {
        TextField("Name", text: $viewModel.name)
    }
}

AnyCancellable is a type-erasing wrapper for Cancellable that stores a subscription cancellation token. Set<AnyCancellable> manages the lifecycle of subscriptions: when the owner is deinitialized, all Cancellable are automatically cancelled. In SwiftUI projects, Set<AnyCancellable> is declared in the ObservableObject class, and subscriptions are added via .store(in: &cancellables). For UIKit, the same mechanisms are used with storage in UIViewController via &cancellables or manual cancel() calls.

Combine vs RxSwift

Combine and RxSwift solve the same reactive programming tasks but have fundamental architectural differences. Combine is part of the Apple SDK with backward compatibility to iOS 13, RxSwift is a third-party library supporting iOS 8+. Combine uses strict error typing through Failure generic, RxSwift uses a single Error type. Combine is integrated with SwiftUI at the platform level, RxSwift requires RxCocoa for UI extensions.

The choice between Combine and RxSwift depends on project requirements. If the minimum iOS version is >= 13 and the project uses SwiftUI — Combine is the natural choice thanks to built-in integration and no additional dependencies. If the project supports iOS 11-12, contains an existing RxSwift codebase, or requires specific operators only available in RxSwift (e.g., Observable.from(path:)), — RxSwift remains a valid solution.

CharacteristicCombineRxSwift
DeveloperApple (built into SDK)ReactiveX (community)
iOS VersioniOS 13+iOS 8+
Error TypeGeneric Failure (Never for UI)Error (any)
UI Integration@Published + SwiftUIRxCocoa + UIKit
Operators~100 built-in400+ operators
Swift ConcurrencyVia .values (async sequence)Via bridge library

Frequently Asked Questions

What is the difference between PassthroughSubject and CurrentValueSubject?

PassthroughSubject does not store state — a subscriber only receives events sent after subscription. CurrentValueSubject stores the current value and passes it to each new subscriber immediately upon subscription. CurrentValueSubject is suitable for representing state (e.g., isLoggedIn).

How to cancel a subscription in Combine?

A subscription returns AnyCancellable, which is cancelled when cancel() is called or upon deinitialization. For group management, use Set<AnyCancellable> — all subscriptions are cancelled when the set is cleared. This is analogous to DisposeBag in RxSwift.

Do I need to learn Combine after async/await?

Yes, Combine remains relevant for streaming data: UI events, debounce, combineLatest, WebSocket. async/await is convenient for one-off requests, Combine — for continuous or multiple streams. Both frameworks are complementary — Publisher can be converted to AsyncSequence.

How to use Combine with UIKit?

UIKit does not have built-in Publishers, but Apple provides extensions: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. For custom UI events, PassthroughSubject or @IBAction wrapped in a Publisher via Future or Subject are used.

What is backpressure in Combine?

Backpressure is a mechanism for controlling the flow rate: Subscriber tells Publisher through Demand how many elements it is ready to process. If Demand = .max(1), Publisher waits for a request before sending the next value. This prevents buffer overflow when producer and consumer speeds mismatch.

Summary

  • Combine is Apple's native reactive framework for iOS 13+ with the Publisher-Subscriber pattern and strict error typing
  • Publisher and Subscriber form a communication channel with Demand management for backpressure
  • Subject (Passthrough and CurrentValue) integrate imperative code into reactive chains
  • Operators map, flatMap, combineLatest, debounce, catch provide declarative stream processing
  • Schedulers via DispatchQueue and RunLoop manage execution threads without blocking UI
  • SwiftUI uses Combine through @Published, ObservableObject, and @StateObject for reactive Views
  • RxSwift remains an alternative for projects with iOS 8+, Combine is the choice for new projects on iOS 13+

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also