Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

Third Edition · iOS 15 · Swift 5.5 · Xcode 13

11. Timers
Written by Florent Pillet

Repeating and non-repeating timers are always useful when coding. Besides executing code asynchronously, you often need to control when and how often a task should repeat.

Before the Dispatch framework was available, developers relied on RunLoop to asynchronously perform tasks and implement concurrency. You could use Timer to create repeating and non-repeating timers. Then, Apple released the Dispatch framework, including DispatchSourceTimer.

Although all of the above are capable of creating timers, not all timers are equal in Combine. Read on!

Using RunLoop

The main thread and any thread you create, preferably using the Thread class, can have its own RunLoop. Just invoke RunLoop.current from the current thread: Foundation would create one for you if needed. Beware, unless you understand how run loops operate — in particular, that you need a loop that runs the run loop — you’ll be better off simply using the main RunLoop that runs the main thread of your application.

Note: One important note and a red light warning in Apple’s documentation is that the RunLoop class is not thread-safe. You should only call RunLoop methods for the run loop of the current thread.

RunLoop implements the Scheduler protocol you’ll learn about in Chapter 17, “Schedulers.” It defines several methods which are relatively low-level, and the only one that lets you create cancellable timers:

let runLoop = RunLoop.main

let subscription = runLoop.schedule(
  after: runLoop.now,
  interval: .seconds(1),
  tolerance: .milliseconds(100)
) {
  print("Timer fired")
}

This timer does not pass any value and does not create a publisher. It starts at the date specified in the after: parameter with the specified interval and tolerance, and that’s about it. Its only usefulness in relation to Combine is that the Cancellable it returns lets you stop the timer after a while.

An example of this could be:

runLoop.schedule(after: .init(Date(timeIntervalSinceNow: 3.0))) {
  subscription.cancel()
}

But all things considered, RunLoop is not the best way to create a timer. You’ll be better off using the Timer class!

Using the Timer class

Timer is the oldest timer that was available in the original Mac OS X, long before Apple renamed it “macOS.” It has always been tricky to use because of its delegation pattern and tight relationship with RunLoop. Combine brings a modern variant you can directly use as a publisher without all the setup boilerplate.

You can create a repeating timer publisher this way:

let publisher = Timer.publish(every: 1.0, on: .main, in: .common)

The two parameters on and in determine:

  • On which RunLoop your timer attaches to. Here, the main thread‘s RunLoop.
  • In which run loop mode(s) the timer runs. Here, the default run loop mode.

Unless you understand how a run loop operates, you should stick with these default values. Run loops are the basic mechanism for asynchronous event source processing in macOS, but their API is a bit cumbersome. You can get a RunLoop for any Thread that you create yourself or obtain from Foundation by calling RunLoop.current, so you could write the following as well:

let publisher = Timer.publish(every: 1.0, on: .current, in: .common)

Note: Running this code on a Dispatch queue other than DispatchQueue.main may lead to unpredictable results. The Dispatch framework manages its threads without using run loops. Since a run loop requires one of its run methods to be called to process events, you would never see the timer fire on any queue other than the main one. Stay safe and target RunLoop.main for your Timers.

The publisher the timer returns is a ConnectablePublisher. It’s a special variant of Publisher that won’t start firing upon subscription until you explicitly call its connect() method. You can also use the autoconnect() operator which automatically connects when the first subscriber subscribes.

Note: You‘ll learn more about connectable publishers in Chapter 13, “Resource Management.”

Therefore, the best way to create a publisher that will start a timer upon subscription is to write:

let publisher = Timer
  .publish(every: 1.0, on: .main, in: .common)
  .autoconnect()

The timer repeatedly emits the current date, its Publisher.Output type being a Date. You can make a timer that emits increasing values by using the scan operator:

let subscription = Timer
  .publish(every: 1.0, on: .main, in: .common)
  .autoconnect()
  .scan(0) { counter, _ in counter + 1 }
  .sink { counter in
    print("Counter is \(counter)")
  }

There is an additional Timer.publish() parameter you didn’t see here: tolerance. It specifies the acceptable deviation from the duration you asked for, as a TimeInterval. But note that using a value lower than your RunLoop’s minimumTolerance value may not produce the expected results.

Using DispatchQueue

You can use a dispatch queue to generate timer events. While the Dispatch framework has a DispatchTimerSource event source, Combine surprisingly doesn’t provide a timer interface to it. Instead, you’re going to use an alternative method to generate timer events in your queue. This can be a bit convoluted, though:

let queue = DispatchQueue.main

// 1
let source = PassthroughSubject<Int, Never>()

// 2
var counter = 0

// 3
let cancellable = queue.schedule(
  after: queue.now,
  interval: .seconds(1)
) {
  source.send(counter)
  counter += 1
}

// 4
let subscription = source.sink {
  print("Timer emitted \($0)")
}

In the previous code, you:

  1. Create a Subject you will send timer values to.
  2. Prepare a counter. You‘ll increment it every time the timer fires.
  3. Schedule a repeating action on the selected queue every second. The action starts immediately.
  4. Subscribe to the subject to get the timer values.

As you can see, this is not pretty. It would help to move this code to a function and pass both the interval and the start time.

Key points

  • Create timers using good old RunLoop class if you have Objective-C code nostalgia.
  • Use Timer.publish to obtain a publisher which generates values at given intervals on the specified RunLoop.
  • Use DispatchQueue.schedule for modern timers emitting events on a dispatch queue.

Where to go from here?

In Chapter 18, “Custom Publishers & Handling Backpressure,” you’ll learn how to write your own publishers, and you’ll create an alternative timer publisher using DispatchSourceTimer.

But don’t hurry! There is plenty to learn before that, starting with Key-Value Observing in the next chapter.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.