What is Combine?

Combine Framework provides an API for processing async events over time such as user-input, network response, and other dynamic data.

What is the purpose of Combine?

The purpose of Combine is to simplify the management of async events and data streams.

Publishers

Publisher declares that a type can transit a sequence of values over time. A publisher delivers elements to one or more Subscriber instances.

class PostService {
    func fetchPosts() -> AnyPublisher<[Post], Error> {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
            fatalError("Invalid URL")
        }
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [Post].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

Subscribers

Subscriber is a protocol that declares a type that can receive input from a publisher. A Subscriber instance receives a stream of elements from a Publisher.

private var cancellable: AnyCancellable?
let service = PostService()
cancellable = service.fetchPosts()
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error.localizedDescription)
        }
    }, receiveValue: { posts in
        print("Received posts count:", posts.count)
    })

AnyCancellable

When you call a method like sink or assign on a publisher, it returns a type that conforms to the Cancellable protocol. Storing this return value in an instance of AnyCancellable keeps the subscription active. When the AnyCancellable instance is deallocated, its deinit method automatically cancels the subscription.

var cancellables = Set<AnyCancellable>()
let publisher = Just("Hello, Combine!")
publisher
    .sink { completion in
        print("Completion: \(completion)")
    } receiveValue: { value in
        print("Received value: \(value)")
    }
    .store(in: &cancellables)

Operators

Transforming Operators

  • map: Transforms each value received from a publisher by applying a function.
  • flatMap: Transforms each value received into a new publisher, then flattens the result into a single publisher stream.
  • scan: Applies a closure over the previous result and the current value to produce a new value, useful for accumulating values.

Filtering Operators

  • filter: Emits only those values from a publisher that satisfy a given predicate.
  • removeDuplicates: Suppresses duplicate consecutive values published by a publisher.
  • first/last: Emits only the first or last value from a publisher that satisfies a predicate condition.

Combining Operators

  • combineLatest: Combines the latest value from two or more publishers and emits a combined value each time any of the publishers emit a value.
  • merge: Combines multiple publishers of the same type into a single publisher stream, emitting values as they arrive.
  • zip: Combines values from multiple publishers into tuples, emitting a tuple only when each of the publishers has emitted a new value.

Error Handling Operators

  • catch: Handles errors from a publisher by replacing the failed publisher with another publisher or a value.
  • retry: Attempts to recreate a subscription to a failed publisher for a specified number of times.

Utility Operators

  • delay: Delays the emission of items from the publisher for a specified interval.
  • subscribe(on:)/receive(on:): Specifies the dispatch queue for performing subscription work or receiving values.
  • print: Prints log messages for all publisher events to the console, useful for debugging.

Timing Operators

  • debounce: Emits a value from a publisher only after a specified time interval has passed without receiving another value.
  • throttle: Emits either the first or last value received in a specified time window.

Collecting Operators

  • collect: Collects received values and emits an array of those values either when the publisher completes or when a buffer size is reached.

When to use Combine?

I found great advice from Apple when it comes to Combine:

“A Combine publisher fills a role similar to, but distinct from, the AsyncSequence in the Swift standard library. A Publisher and an AsyncSequence both produce elements over time. However, the pull model in Combine uses a Subscriber to request elements from a publisher, while Swift concurrency uses the for-await-in syntax to iterate over elements published by an AsyncSequence. Both APIs offer methods to modify the sequence by mapping or filtering elements, while only Combine provides time-based operations like debounce(for:scheduler:options:) and throttle(for:scheduler:latest:), and combining operations like merge(with:) and combineLatest(_:_:). To bridge the two approaches, the property values exposes a publisher’s elements as an AsyncSequence, allowing you to iterate over them with for-await-in rather than attaching a Subscriber.”

Thank you for reading!