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
The search text changes five times while you type one word.

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
What a search screen does each time the text changes.

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.

Key idea

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
You subscribe once; new videos come to you whenever they're published.

Combine works exactly the same way. Only the names change:

Publisher
    ↓
 publishes
    ↓
  Value
    ↓
Subscriber
The channel is the Publisher, the new video is the value, and you are the 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
One publisher, four values, arriving one after another.

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"
A search field as a publisher of text values.

And a network request moves through stages over time as well:

Request
   ↓
Loading
   ↓
Response
A network request as a sequence of events.

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 screen is the subscriber.

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

Note

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!
Console output.

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()
The value travels from 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
Steps can be fitted along the pipe between publisher and 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
Console output.

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
Each value travels down the pipe on its own.

This points to the biggest mental shift in Combine. Normal code often feels like a list of instructions:

Do this
then this
then this
Step-by-step thinking.

Combine encourages you to think differently:

When a value arrives
        ↓
do something with it.
Reactive thinking.
Key idea

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
Console output.

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(...)
Every step is one small, readable job.

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.

Note

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
Key idea

Put the three together and you have the whole Combine mental model:

Publisher
    ↓
 Values
    ↓
Operators
    ↓
Subscriber

Tiny Challenge

Your turn

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.