Combine for Beginners · Lesson 1 of 9
Understanding Combine
Before any operators or APIs, this lesson builds the one idea everything in Combine rests on: values that change over time, flowing from a publisher to a subscriber. By the end, you'll have written your first Combine code.
Every Keystroke Is an Event
Picture a search box in an app. You start typing the word "Swift", one letter at a time:
S Sw Swi Swif Swift
Every time the text changes, something happens. A good search screen doesn't fire off a search for every single letter. It waits for a moment of calm, then searches, then shows the results:
User types
↓
Text changes
↓
Wait briefly
↓
Search
↓
Receive results
↓
Update screen
Notice what's really going on here. The search text isn't a single value you read once — it's a value that keeps changing over time, and the app needs to react each time it does.
Values changing over time are at the heart of Combine.
By the end of this lesson you'll understand this simple chain — and you'll have written your first Combine code that uses it:
Publisher
↓
Value
↓
Subscriber
What Exactly Is Combine?
The official description is that Combine is Apple's framework for working with asynchronous events and changing values. That's accurate, but it doesn't help much when you're new to it. So let's use something you already know.
Think about subscribing to a YouTube channel. Once you subscribe, you don't keep checking the channel yourself. Whenever the channel publishes a new video, it simply arrives for you:
YouTube Channel
↓
publishes
↓
New Video
↓
You
Combine works exactly the same way. Only the names change:
Publisher
↓
publishes
↓
Value
↓
Subscriber
Keep this picture in mind. Almost everything else in this course is a variation of it.
Publisher
A Publisher produces values. The important part is that those values can change over time.
Imagine a publisher that reports the temperature outside:
Temperature Publisher 24°C ↓ 25°C ↓ 27°C ↓ 26°C
It isn't one number — it's a stream of numbers, each arriving when the temperature changes. The search box from earlier is the same idea, with text instead of numbers:
"S" ↓ "Sw" ↓ "Swi" ↓ "Swift"
And a network request moves through stages over time as well:
Request ↓ Loading ↓ Response
A temperature, some text, a network request — very different things. But they have one thing in common: they all involve events happening over time. That is exactly the kind of thing a publisher represents.
Subscriber
If a publisher produces values, somebody needs to receive them. That somebody is the Subscriber.
Publisher
│
│ sends value
↓
Subscriber
Back to the temperature example. Suppose the screen of a weather app is subscribed to the temperature publisher:
Temperature Publisher
│
│ 27°C
↓
Screen
The publisher is effectively saying: "The temperature is now 27°C." The subscriber — the screen — receives that value and reacts to it, for example by updating the label that shows the temperature. When the next value arrives, it reacts again.
Your First Combine Code
The easiest place to try these examples is an Xcode Playground: File → New → Playground, choose Blank, and paste the code in. The output appears in the console at the bottom.
Every Combine file starts by importing the framework:
import Combine
Now let's create a publisher:
let publisher = Just("Hello Combine!")
Don't worry about the details of Just yet.
For now, think of it as a very simple publisher that has
exactly one value to give:
Just │ │ publishes ↓ "Hello Combine!"
But if you run this code, nothing happens. Nothing is printed and nothing changes. Why? Because nobody is listening yet. It's a YouTube channel with no subscribers — the video exists, but nobody is there to receive it.
Listening with sink
To listen to a publisher, we attach a subscriber. The
simplest way to do that is with sink:
let publisher = Just("Hello Combine!")
publisher
.sink { value in
print(value)
}
Run it, and the console shows:
Hello Combine!
The closure you give to sink runs whenever a
value arrives. Here the value is
"Hello Combine!", and we print it.
That's it — you've just created your first Combine pipeline.
What Actually Happened?
Let's slow down and trace the value through the code:
Just("Hello Combine!")
│
│ publishes
↓
sink
│
↓
print()
Just, to sink, to print().Strip away the details, and it's the same picture as before:
Publisher → Subscriber
Just("Hello Combine!")is the Publisher. It produces the value.sink { … }is the Subscriber. It listens for the published value and decides what to do with it — in this case, print it.
Combine as a Pipe
Here's a second way to picture Combine that becomes very useful as pipelines grow: think of it as a water pipe.
At one end there's a source — a tank that releases water. At the other end there's a tap where you collect it. The water flows through the pipe from one end to the other. In Combine, the publisher is the source, the values are the water, and the subscriber is the tap at the end:
Publisher (the source)
↓
Value (what flows through)
↓
Subscriber (where it comes out)
Real water pipes often have things fitted along the way — a filter that cleans the water, a heater that warms it — so what comes out of the tap isn't quite what went in. Combine pipes work the same way. Between the publisher and the subscriber, you can change values or let only some of them through:
Publisher
↓
Value
↓
Transform
↓
Filter
↓
Subscriber
These steps along the pipe are called operators. Combine has many of them, but you don't need a list right now — you'll meet them one at a time, when they solve a real problem. You'll use your first one later in this lesson.
Multiple Values
Just publishes only one value. But the whole
point of Combine is values over time — so let's try a
publisher that sends several:
[1, 2, 3, 4, 5].publisher
.sink { number in
print(number)
}
Adding .publisher to an array turns it into a
publisher that sends each element, one after another. The
output is:
1 2 3 4 5
The sink closure didn't run once — it ran
five times, once for each value as it
arrived:
Publisher
│
├── 1 ──→ sink → prints 1
├── 2 ──→ sink → prints 2
├── 3 ──→ sink → prints 3
├── 4 ──→ sink → prints 4
└── 5 ──→ sink → prints 5
This points to the biggest mental shift in Combine. Normal code often feels like a list of instructions:
Do this then this then this
Combine encourages you to think differently:
When a value arrives
↓
do something with it.
In Combine you don't ask for values; you describe what should happen whenever a value arrives — whether that's one value, five, or a new one every time the user types.
Your First Transformation
Here's a small problem. A publisher sends the number:
5
But the subscriber should receive:
10
We need something along the pipe that doubles the value on
its way through. That's the job of the map
operator:
Just(5)
.map { number in
number * 2
}
.sink { result in
print(result)
}
The result:
10
Trace the value from top to bottom:
Just(5)
↓
Publisher
↓
5
↓
map { × 2 }
↓
10
↓
sink
map takes each value, changes it, and passes the new value on.
map sits between the publisher and the
subscriber, exactly where the pipe diagram said operators
would go.
Reading longer pipelines
As you explore Combine code in real apps, you'll come across pipelines that look like this:
publisher
.map(...)
.filter(...)
.debounce(...)
.sink(...)
At first glance that can look intimidating. But you already know how to read it: a value comes out of the publisher and flows down through each step in order. Conceptually, it's just this:
VALUE ↓ change it .map(...) ↓ check it .filter(...) ↓ wait .debounce(...) ↓ receive it .sink(...)
You'll learn filter and
debounce properly in later lessons — the point
for now is that a long pipeline is only a series of small
steps along the same pipe.
Keeping a Subscription
If you type the sink examples into an app
project, Xcode may show a warning like:
Result of call to 'sink(receiveValue:)' is unused.
That's because sink returns something: the
subscription itself.
The usual first step is to store it in a constant:
let cancellable = Just("Hello")
.sink { value in
print(value)
}
The value stored in cancellable is an
AnyCancellable. For now, remember one simple
rule: a Combine subscription needs to be kept alive
for as long as you want to receive values. Holding
on to it is how you keep it alive.
That's all you need about subscriptions for now. Lesson 5, Managing Subscriptions, covers exactly how long they live, how to store several of them, and how to stop them properly.
Three Ideas to Remember
Everything in this lesson comes down to three ideas.
1. Publisher
Produces values.
Publisher ↓ value
2. Subscriber
Receives values.
value
↓
Subscriber
3. Operator
Does something to values while they travel between them.
Publisher
↓
value
↓
Operator
↓
new value
↓
Subscriber
Put the three together and you have the whole Combine mental model:
Publisher
↓
Values
↓
Operators
↓
Subscriber
Tiny Challenge
Look at this pipeline and work out what it prints before you run it:
[1, 2, 3, 4].publisher
.map { number in
number * 10
}
.sink { value in
print(value)
}
Trace each value through the pipe, one at a time:
1 → ×10 → ? 2 → ×10 → ? 3 → ×10 → ? 4 → ×10 → ?
Write your answer down, then paste the code into a Playground and see whether the output matches. If it doesn't, trace it again and find the step where your prediction changed.