0Pricing
Swift Academy · Lesson

Schedulers and Threading

Control execution with receive(on:) and subscribe(on:).

Schedulers and Threading is a free Swift Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Swift Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Schedulers in Combine

A scheduler in Combine defines when and where work runs — which thread or queue executes the operators and delivers values to subscribers.

Controlling schedulers is essential for keeping UI updates on the main thread and heavy work off it.

Two Key Operators

Combine gives you two operators to control threading:

  • subscribe(on:) — controls where the subscription and upstream work begins
  • receive(on:) — controls where downstream values are delivered

They affect different ends of the pipeline.

receive(on:)

receive(on:) moves everything after it onto the given scheduler. The most common use is hopping back to the main queue before updating UI.

apiClient.fetchUser(id: 1)
    .receive(on: DispatchQueue.main)
    .sink { user in
        self.nameLabel.text = user.name
    }
    .store(in: &cancellables)

subscribe(on:)

subscribe(on:) controls the thread on which the publisher starts producing and where upstream operators run. Use it to push expensive setup off the main thread.

heavyPublisher
    .subscribe(on: DispatchQueue.global(qos: .userInitiated))
    .receive(on: DispatchQueue.main)
    .sink { result in
        self.update(result)
    }
    .store(in: &cancellables)

subscribe(on:) vs receive(on:)

Remember the directions:

  • subscribe(on:) affects upstream — where work starts
  • receive(on:) affects downstream — where results are delivered

A typical pattern uses subscribe(on:) for background work and receive(on:) for main-thread delivery.

DispatchQueue as Scheduler

DispatchQueue conforms to the Scheduler protocol. DispatchQueue.main delivers on the main thread; global queues run concurrently in the background.

let background = DispatchQueue.global(qos: .background)

publisher
    .map { expensiveTransform($0) }
    .subscribe(on: background)
    .receive(on: DispatchQueue.main)
    .sink { self.render($0) }
    .store(in: &cancellables)

RunLoop as Scheduler

RunLoop is another scheduler, tied to a thread's run loop. RunLoop.main delivers in coordination with UI events, which can matter for things like scroll responsiveness.

timerPublisher
    .receive(on: RunLoop.main)
    .sink { _ in
        self.tick()
    }
    .store(in: &cancellables)

DispatchQueue vs RunLoop

Both can deliver on the main thread, but they differ subtly:

  • DispatchQueue.main — schedules even during scrolling tracking
  • RunLoop.main — defers work while the run loop is in tracking mode

For most UI updates after async work, DispatchQueue.main is the safer default.

Scheduler Timing Operators

Operators like debounce, throttle, and delay all take a scheduler argument that decides which queue handles their timing.

searchTextPublisher
    .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
    .sink { query in
        self.runSearch(query)
    }
    .store(in: &cancellables)

Common Threading Bug

Updating UI off the main thread causes crashes or glitches. If you forget receive(on: DispatchQueue.main) after a background publisher, your sink may run on a background thread.

Always deliver UI-facing values on the main scheduler.

Putting It Together

A robust pipeline starts work in the background, processes there, then delivers to the main thread for display.

imageURLPublisher
    .subscribe(on: DispatchQueue.global())
    .flatMap { url in downloadImage(url) }
    .map { resize($0) }
    .receive(on: DispatchQueue.main)
    .sink { self.imageView.image = $0 }
    .store(in: &cancellables)

Quick Check

Test your scheduler knowledge.

Recap

You learned to control threading in Combine:

  • subscribe(on:) — where upstream work starts
  • receive(on:) — where results are delivered
  • DispatchQueue and RunLoop as schedulers
  • Always deliver UI updates on the main thread

Proper scheduling keeps apps responsive and crash-free.

Frequently asked questions

Is the “Schedulers and Threading” lesson free?

Yes — the full text of “Schedulers and Threading” is free to read here on the web, and the Swift Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Swift Academy course, upgrade to CoddyKit PRO.

What will I learn in “Schedulers and Threading”?

Control execution with receive(on:) and subscribe(on:). You practise Swift Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Swift Academy?

No prior experience is required. Swift Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Schedulers and Threading” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Swift Academy lesson?

Yes. Every Swift Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Transforming and Combining Streams
  2. Schedulers and Threading
  3. Error Handling Operators
  4. Custom Publishers and Subscribers
← Back to Swift Academy