Skip to content
advanced Phase 9 · State Management & Architecture Patterns

Combine Framework

Use publishers, subscribers, operators, and subjects for reactive data flow in iOS.

1h
4 problems
Topic Progress 0%

Publishers & Subscribers

Understanding Combine Architecture

Combine is Apple's reactive programming framework. It uses three core types: Publishers emit values, Subscribers receive values, and Operators transform values between them.

Creating Publishers

Use Just, Future, or URLSession publishers to emit values.

import Combine

// Just emits a single value and completes
let publisher = Just("Hello Combine")
let _ = publisher.sink { value in
    print(value) // "Hello Combine"
}

// Future emits a value asynchronously
let future = Future<String, Error> { promise in
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        promise(.success("Async value"))
    }
}
let _ = future.sink(
    receiveCompletion: { _ in },
    receiveValue: { print($0) }
)

// URLSession publisher for network requests
let url = URL(string: "https://api.example.com/data")!
let dataPublisher = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: MyModel.self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)

Subscribing to Publishers

Subscribers receive values and completion events from publishers.

let cancellable = publisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Completed")
        case .failure(let error):
            print("Error: \(error)")
        }
    },
    receiveValue: { value in
        print("Received: \(value)")
    }
)

// Store cancellables to prevent deallocation
var cancellables = Set<AnyCancellable>()
publisher.sink { print($0) }.store(in: &cancellables)

Managing Subscription Lifecycle

Always store cancellables to keep subscriptions alive.

class ViewModel: ObservableObject {
    @Published var data: [String] = []
    private var cancellables = Set<AnyCancellable>()
    
    func fetchData() {
        URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [String].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { _ in },
                receiveValue: { [weak self] items in
                    self?.data = items
                }
            )
            .store(in: &cancellables)
    }
}

Operators

Transforming Values

Operators modify, filter, and combine values emitted by publishers.

let numbers = [1, 2, 3, 4, 5].publisher

// Map transforms each value
numbers
    .map { $0 * 2 }
    .sink { print($0) } // 2, 4, 6, 8, 10

// Filter keeps values matching a condition
numbers
    .filter { $0 % 2 == 0 }
    .sink { print($0) } // 2, 4

// Reduce combines all values into one
numbers
    .reduce(0, +)
    .sink { print($0) } // 15

// Remove duplicates
[1, 1, 2, 2, 3].publisher
    .removeDuplicates()
    .sink { print($0) } // 1, 2, 3

Combining Publishers

Merge, combine, and zip multiple publishers together.

let publisher1 = [1, 2, 3].publisher
let publisher2 = ["A", "B", "C"].publisher

// CombineLatest emits when either publisher emits
publisher1.combineLatest(publisher2)
    .sink { num, letter in
        print("\(num)\(letter)") // 1A, 2A, 3A, 3B, 3C
    }

// Zip pairs values from both publishers
publisher1.zip(publisher2)
    .sink { num, letter in
        print("\(num)\(letter)") // 1A, 2B, 3C
    }

// Merge combines all values as they arrive
publisher1.merge(with: publisher2)
    .sink { print($0) }

Timing and Scheduling

Control when values are emitted and received.

let publisher = [1, 2, 3].publisher

// Delay emissions
publisher
    .delay(for: .seconds(2), scheduler: DispatchQueue.main)
    .sink { print($0) }

// Throttle rapid emissions
publisher
    .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
    .sink { print($0) }

// Receive on specific scheduler
publisher
    .receive(on: DispatchQueue.main)
    .sink { print($0) }

Error Handling

Handle errors gracefully in Combine chains.

let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .mapError { $0 as Error }
    .retry(3) // Retry 3 times on failure
    .catch { error in
        // Return fallback value
        return Just(Data())
    }
    .sink { data in
        print("Received data")
    }

Subjects & PassthroughSubject

What Are Subjects?

Subjects are publishers that allow you to manually send values. They bridge imperative code to reactive streams.

PassthroughSubject

Emits values to subscribers but does not store them. New subscribers miss previous values.

import Combine

let subject = PassthroughSubject<String, Never>()

let cancellable = subject.sink { value in
    print("Received: \(value)")
}

subject.send("Hello")
subject.send("World")
// Both values are received

// New subscriber misses previous values
let cancellable2 = subject.sink { value in
    print("New subscriber received: \(value)")
}
subject.send("After new subscriber")
// Only "After new subscriber" is received by cancellable2

CurrentValueSubject

Stores the current value and emits it to new subscribers immediately.

let currentValue = CurrentValueSubject<Int, Never>(0)

let cancellable = currentValue.sink { value in
    print("Value: \(value)")
} // Prints 0 immediately

currentValue.send(1)
let cancellable2 = currentValue.sink { value in
    print("New subscriber: \(value)")
} // Prints 1 immediately

Creating Custom Publishers

Build your own publishers for complex data sources.

struct TimerPublisher: Publisher {
    typealias Output = Date
    typealias Failure = Never
    
    let interval: TimeInterval
    
    func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
        let subscription = TimerSubscription(
            subscriber: subscriber,
            interval: interval
        )
        subscriber.receive(subscription: subscription)
    }
}

class TimerSubscription<S: Subscriber>: Subscription where S.Input == Date {
    private var subscriber: S?
    private var timer: Timer?
    
    init(subscriber: S, interval: TimeInterval) {
        self.subscriber = subscriber
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
            _ = self?.subscriber?.receive(Date())
        }
    }
    
    func request(_ demand: Subscribers.Demand) { }
    func cancel() {
        timer?.invalidate()
        subscriber = nil
    }
}

Using Subjects in ViewModels

Subjects are ideal for connecting UI events to business logic.

class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var results: [Result] = []
    private var cancellables = Set<AnyCancellable>()
    private let searchSubject = PassthroughSubject<String, Never>()
    
    init() {
        searchSubject
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .flatMap { query in
                APIClient.search(query: query)
                    .catch { _ in Just([]) }
            }
            .receive(on: DispatchQueue.main)
            .assign(to: &$results)
    }
    
    func search(_ query: String) {
        searchSubject.send(query)
    }
}

Quiz

1. What is the difference between PassthroughSubject and CurrentValueSubject?

Question 1 options

2. What does the sink operator do?

Question 2 options

3. How do you prevent a Combine subscription from being deallocated?

Question 3 options

4. What does the debounce operator do?

Question 4 options

Flashcards

Question

What is a Publisher in Combine?

Answer

A type that emits values over time to subscribers, forming the data source of a Combine pipeline.

Question

What is AnyCancellable used for?

Answer

It represents a subscription that can be cancelled. Store in a Set to manage subscription lifecycle.

Question

What does .assign(to:) do?

Answer

Automatically assigns received values to a property using key path, commonly used with @Published properties.

Question

When would you use PassthroughSubject?

Answer

When you need to manually send values to subscribers, such as bridging UI events to reactive streams.

Revision Notes

Key Takeaways

  • 1. Publishers emit values, subscribers receive them
  • 2. Operators transform, filter, and combine streams
  • 3. Store AnyCancellable to manage subscription lifecycle
  • 4. PassthroughSubject for manual emission, CurrentValueSubject stores values
  • 5. Combine integrates naturally with @Published in SwiftUI

Interview Tips

  • Explain the Publisher-Subscriber-Operator architecture
  • Discuss when to use Combine vs async/await
  • Describe how debounce and throttle differ

Cheat Sheet

Combine Quick Reference

  • Just(value) - Emit single value
  • Future - Async value emission
  • .sink() - Subscribe and receive values
  • .assign(to:) - Assign to property
  • .map() / .filter() / .reduce() - Transform values
  • .combineLatest() / .zip() / .merge() - Combine publishers
  • .debounce() / .throttle() - Timing operators
  • PassthroughSubject - Manual value sending
  • CurrentValueSubject - Stores current value
  • AnyCancellable - Subscription management