Hello! Welcome to the third lesson in our "Modern Architecture with MVVM" module.
In the previous lesson, we built the "glue" for MVVM by implementing data binding from scratch. We saw how to use both simple closures and a more scalable, generic Observable class to allow our ViewModel to update the View without a direct dependency. These patterns are foundational, but they are just the beginning of reactive programming.
What happens when you need to handle more complex streams of events? Imagine fetching data from a network, debouncing user input from a search bar, or chaining multiple asynchronous operations. Building these from scratch would be complex and error-prone. This is where a dedicated framework becomes essential.
Today, we will dive into Apple's native solution for this: the Combine framework. This lesson directly addresses our learning outcome: to explain the core concepts of the Combine framework: Publishers, Subscribers, and Operators. By the end, you'll understand the building blocks that enable modern, reactive iOS development.
What is Combine and Functional Reactive Programming?
Before we jump into the components, let's establish the "why." Combine is Apple's framework for Functional Reactive Programming (FRP). At its core, FRP is a paradigm for declaratively processing values that change over time. These "values over time" can be anything: user taps, network responses, timer events, or property changes.
To get a better sense of this paradigm, let's start with a brief reading.
This article by Donny Wals, 'Getting started with Combine', provides a clear, high-level introduction to what Combine is and the FRP paradigm it enables. Understanding this context is key to grasping how the different components fit together.
Read the introduction and the section 'Understanding what Functional Reactive Programming is'. Focus on the concept of data flowing from one place to another automatically through subscriptions.
As the article describes, Combine allows you to define a processing pipeline for asynchronous events, making your code easier to read and reason about compared to nested completion closures (often called "callback hell").
The Three Pillars of Combine
At the heart of Combine are three main concepts that work together:
- Publisher: An object that emits a sequence of values over time.
- Subscriber: An object that receives values from a Publisher.
- Operator: A method that transforms values emitted by a Publisher before they reach the Subscriber.
This interaction forms a chain, or pipeline, for data to flow through.

Let's break down each of these components in detail.
Publishers and Subscribers: The Source and Sink
A Publisher defines how values and errors are produced. It's a protocol with two important associated types:
Output: The type of value the publisher emits.Failure: The type of error the publisher can emit. If it can never fail, this isNever.
A Subscriber is the consumer of those values. It also has two associated types that must match the publisher it subscribes to:
Input: The type of value it can receive (must match the Publisher'sOutput).Failure: The type of error it can receive (must match the Publisher'sFailure).
A publisher will not emit any values until it has at least one subscriber. The entire process follows a well-defined lifecycle.
To see this lifecycle in action, let's watch a video that explains these concepts visually.
Publishers and Subscribers - Reactive Programming in iOS with Combine - raywenderlich.com
This video from Kodeco provides an excellent breakdown of Publishers and Subscribers, their underlying protocols, and the lifecycle of a subscription.
Please watch from the beginning until 03:56. Focus on: The analogy of a Publisher to NotificationCenter. The marble diagram visualization of value streams, completions, and failures. The roles of the Publisher and Subscriber protocols and their associated types. The concept of a Subscription and how it manages the connection.
As the video explained, a subscription is created when a subscriber attaches to a publisher. This subscription can emit zero or more values, but it will terminate with at most one completion event—either .finished or .failure(Error). After a completion event, the subscription is over and no more values will be sent.
Creating a Subscription and Managing Its Lifetime
While you can create custom types that conform to the Subscriber protocol, Combine provides two incredibly convenient built-in subscribers that you will use most of the time:
sink(receiveCompletion:receiveValue:): Creates a subscriber that executes closures you provide for handling new values and the completion event.assign(to:on:): Creates a subscriber that directly assigns each received value to a property on a given object.
When you call one of these methods on a publisher, it returns an object that conforms to the Cancellable protocol. This object represents the subscription. If this object is deallocated, the subscription is automatically cancelled. This is a critical memory management detail.
Properly managing the lifecycle of your subscriptions is essential to avoid both memory leaks and subscriptions that terminate prematurely. Let's return to the Donny Wals article to see how this is done.
Read the subsection titled 'Keeping track of subscriptions'. Pay close attention to the role of AnyCancellable and the common practice of storing these objects in a set.
The key takeaway is that you must hold a strong reference to the AnyCancellable object returned by sink or assign for as long as you want to receive values. The standard practice is to have a property like private var cancellables = Set<AnyCancellable>() in your class and call .store(in: &cancellables) on the subscription.
Operators: The Processing Pipeline
Operators are where the "functional" part of FRP shines. An operator is a method on a publisher that returns a new publisher. It sits between the original (upstream) publisher and the final subscriber, transforming the data in some way.
You can chain multiple operators together to create a declarative pipeline. Here are a few common operators:
map: Transforms each value into a new value, similar toArray.map.filter: Emits only the values that satisfy a given condition.debounce: Publishes only after a specified time has passed without any new values being emitted. (Perfect for search bars!)receive(on:): Specifies the scheduler (e.g., the main thread) on which to deliver values to the subscriber.
Let's look at a practical example of an operator in action.
Getting started with the Combine framework in Swift
This article by Antoine van der Lee provides a clear, practical example of using the .map operator to make a publisher's output useful for a subscriber.
Read the section 'The Foundation framework and Combine'. Observe how NotificationCenter.Publisher emits a full Notification object, but the UI only needs a String?. The .map operator is used to bridge this gap by extracting the title from the notification's object.
Putting It All Together
Let's consolidate everything we've learned into a single, complete code example. Here, we will:
- Create a publisher for a custom notification.
- Use the
mapoperator to extract aStringfrom the notification'suserInfo. - Use the
sinksubscriber to receive the final string and print it. - Store the subscription in a set of
AnyCancellables.
import Combine
import Foundation
// A class to hold our subscription
class MyObserver {
// 4. A set to hold subscriptions, ensuring they are not deallocated prematurely.
private var cancellables = Set<AnyCancellable>()
// Define a custom notification name
static let myNotification = Notification.Name("com.tutor.myNotification")
init() {
// 1. Create a publisher from NotificationCenter for our custom notification.
// Its Output is `Notification`, and its Failure is `Never`.
let notificationPublisher = NotificationCenter.default.publisher(for: Self.myNotification)
// 2. Use the `map` operator to transform the output.
// The upstream publisher emits `Notification`, but we want a `String`.
let messagePublisher = notificationPublisher.map { notification -> String in
// Safely extract the message from userInfo.
return (notification.userInfo?["message"] as? String) ?? "No message"
}
// 3. Subscribe using `sink`.
let subscription = messagePublisher.sink { receivedValue in
print("Received message: \(receivedValue)")
}
// 4. Store the subscription.
subscription.store(in: &cancellables)
}
deinit {
print("MyObserver is being deallocated. Subscriptions will be cancelled.")
}
}
// --- Let's test it ---
print("Creating observer...")
var observer: MyObserver? = MyObserver()
print("Posting first notification...")
NotificationCenter.default.post(name: MyObserver.myNotification, object: nil, userInfo: ["message": "Hello, Combine!"])
print("Posting second notification...")
NotificationCenter.default.post(name: MyObserver.myNotification, object: nil, userInfo: ["message": "Reactive programming is powerful."])
print("Deallocating observer...")
observer = nil // This will deallocate the observer and cancel the subscription.
print("Posting third notification (should not be received)...")
NotificationCenter.default.post(name: MyObserver.myNotification, object: nil, userInfo: ["message": "This won't be seen."])
/*
Expected Output:
Creating observer...
Posting first notification...
Received message: Hello, Combine!
Posting second notification...
Received message: Reactive programming is powerful.
Deallocating observer...
MyObserver is being deallocated. Subscriptions will be cancelled.
Posting third notification (should not be received)...
*/
This example encapsulates the entire flow: a publisher emits values, an operator transforms them, a subscriber receives them, and an AnyCancellable manages the subscription's lifetime.
Conclusion
In this lesson, we demystified the core of Apple's Combine framework. You now have a solid conceptual foundation for thinking reactively.
Key Takeaways:
- Combine is Apple's framework for declarative, functional reactive programming, designed to handle asynchronous streams of values.
- Publishers are the source of values. They have an
Outputtype and aFailuretype. - Subscribers are the consumers of values. They receive values and a single completion event. The
sinkmethod is a convenient way to create a subscriber. - Operators are chained together to create a processing pipeline that transforms values between the publisher and subscriber.
AnyCancellableis the key to memory management. You must retain the cancellable object returned from a subscription to keep it alive.
Preview of the Next Lesson:
Now that we understand the fundamentals, it's time to apply them to our MVVM architecture. In the next lesson, we will refactor our project to replace our custom Observable class with Combine's @Published property wrapper and the assign(to:) subscriber. This will make our data binding code even more concise and powerful.