Swift
Timer
Cancel
iOS Development
Programming Tips

Cancel a timed event in Swift?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Timed events in Swift are useful for delayed UI actions, polling, retry logic, and scheduled background work. Just as important as starting a timer is canceling it at the right moment to avoid stale actions and memory leaks. This guide covers practical cancellation patterns for Timer, DispatchWorkItem, and DispatchSourceTimer.

Canceling Timer with invalidate

For Foundation timers, cancellation is done with invalidate. Always keep a strong reference so you can stop it later.

swift
1import Foundation
2
3final class CountdownController {
4    private var timer: Timer?
5    private var remaining = 5
6
7    func start() {
8        timer?.invalidate()
9        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
10            guard let self else { return }
11            self.remaining -= 1
12            print("Remaining: \(self.remaining)")
13            if self.remaining <= 0 {
14                self.cancel()
15            }
16        }
17    }
18
19    func cancel() {
20        timer?.invalidate()
21        timer = nil
22    }
23}

Setting the reference to nil after invalidation helps prevent accidental reuse.

Canceling Delayed Tasks with DispatchWorkItem

If you schedule one delayed closure on a queue, DispatchWorkItem is often cleaner than Timer.

swift
1import Foundation
2
3final class SearchDebouncer {
4    private var pending: DispatchWorkItem?
5
6    func scheduleSearch(text: String) {
7        pending?.cancel()
8
9        let item = DispatchWorkItem {
10            print("Execute search for: \(text)")
11        }
12
13        pending = item
14        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: item)
15    }
16
17    func cancel() {
18        pending?.cancel()
19        pending = nil
20    }
21}

This is ideal for debouncing user input where newer requests should cancel older ones.

Repeating Timers with DispatchSourceTimer

For advanced scheduling or background queue execution, use DispatchSourceTimer.

swift
1import Foundation
2
3final class Heartbeat {
4    private var source: DispatchSourceTimer?
5
6    func start() {
7        cancel()
8
9        let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility))
10        timer.schedule(deadline: .now(), repeating: .seconds(5))
11        timer.setEventHandler {
12            print("heartbeat tick")
13        }
14        timer.resume()
15        source = timer
16    }
17
18    func cancel() {
19        source?.setEventHandler {}
20        source?.cancel()
21        source = nil
22    }
23}

Clear event handlers before cancellation when closures capture external resources.

Lifecycle-Aware Cancellation

In iOS, cancel timed work when a view controller disappears or deinitializes. This prevents actions from firing on screens that are no longer visible.

swift
1override func viewWillDisappear(_ animated: Bool) {
2    super.viewWillDisappear(animated)
3    debouncer.cancel()
4    countdown.cancel()
5}
6
7deinit {
8    debouncer.cancel()
9    countdown.cancel()
10}

Lifecycle-aware cleanup is one of the easiest ways to prevent subtle UI bugs.

Choosing the Right Timing Primitive

Use Timer when work is tied to the main run loop, such as user interface updates. Use DispatchWorkItem for cancelable one-shot delays like search debouncing. Use DispatchSourceTimer for precise repeating work on background queues.

Choosing the right primitive early prevents awkward refactors later. If a feature starts as UI-only but grows into background polling, moving from Timer to dispatch-based timing can improve control and reduce run loop coupling. Document this decision so future contributors keep behavior consistent across modules.

Concurrency Safety Note

If timers are controlled from several asynchronous callbacks, move start and cancel operations onto a single serial queue. That avoids duplicate scheduling and hard-to-reproduce timing bugs in production.

Common Pitfalls

A common pitfall is creating timers without storing references. If you cannot access the timer object later, cancellation becomes impossible.

Another issue is retain cycles from strong captures in timer closures. Use weak captures where appropriate and verify objects deallocate as expected.

Developers also forget that invalidating a Timer must occur on the relevant run loop context for predictable behavior. Keep timer ownership clear.

Finally, cancellation race conditions can occur when multiple threads start and stop the same timed event. Centralize control on one queue or protect state with synchronization.

Summary

  • Cancel Timer instances with invalidate, then clear the reference.
  • Use DispatchWorkItem for cancelable one-shot delayed tasks.
  • Use DispatchSourceTimer for queue-based repeating schedules.
  • Tie cancellation to view and object lifecycle events.
  • Prevent retain cycles and race conditions with explicit ownership rules.

Course illustration
Course illustration

All Rights Reserved.