Practical examples of timer examples with Grand Central Dispatch in Swift

If you’re building anything non-trivial in iOS or macOS, you will eventually need reliable timers. And that’s where Grand Central Dispatch (GCD) shines. In this guide, we’ll walk through practical, real-world examples of timer examples with Grand Central Dispatch in Swift, from simple repeating timers to more advanced patterns like debouncing and background-safe scheduling. Instead of just showing one basic example of a repeating timer and calling it a day, we’ll look at how production apps actually use GCD timers: updating UI at intervals, rate-limiting network calls, scheduling analytics pings, and more. These examples include both main-queue timers for UI work and background-queue timers for heavy lifting. If you’ve ever wrestled with `Timer.scheduledTimer` and run into retain cycles, missed fires, or odd behavior when the app goes to the background, you’ll see why GCD timers are often the better choice. By the end, you’ll have a solid toolkit of the best examples and patterns you can reuse across your Swift projects.
Written by
Jamie
Published

Real-world examples of timer examples with Grand Central Dispatch in Swift

Let’s skip the theory and start with actual code. All of these examples of timer examples with Grand Central Dispatch in Swift use DispatchSourceTimer, which is the go-to GCD API for timers.

Basic repeating GCD timer on the main queue

This is the classic example of a repeating timer that updates the UI every second, such as a countdown label or clock.

final class CountdownViewModel {
    private var timer: DispatchSourceTimer?
    private let queue = DispatchQueue.main
    private var remainingSeconds: Int

    init(startSeconds: Int) {
        self.remainingSeconds = startSeconds
    }

    func start(onTick: @escaping (Int) -> Void, onComplete: (() -> Void)? = nil) {
        timer?.cancel()

        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.schedule(deadline: .now(), repeating: 1.0)

        timer.setEventHandler { [weak self] in
            guard let self = self else { return }
            self.remainingSeconds -= 1
            onTick(self.remainingSeconds)

            if self.remainingSeconds <= 0 {
                self.timer?.cancel()
                self.timer = nil
                onComplete?()
            }
        }

        self.timer = timer
        timer.resume()
    }

    func stop() {
        timer?.cancel()
        timer = nil
    }
}

This is a clean example of a UI-focused timer that:

  • Runs on DispatchQueue.main so UI updates are safe.
  • Cancels itself when done.
  • Avoids strong reference cycles with [weak self].

Among the best examples of timer examples with Grand Central Dispatch in Swift, this pattern shows up constantly in countdowns, OTP screens, and workout timers.

Background GCD timer for periodic work

Now a more production-like example of work that should not run on the main queue: say you’re cleaning a cache every 10 minutes.

final class CacheCleaner {
    private var timer: DispatchSourceTimer?
    private let queue = DispatchQueue(label: "com.example.cachecleaner")

    func start() {
        timer?.cancel()

        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.schedule(deadline: .now() + 10 * 60, repeating: 10 * 60)

        timer.setEventHandler { [weak self] in
            self?.cleanExpiredItems()
        }

        self.timer = timer
        timer.resume()
    }

    func stop() {
        timer?.cancel()
        timer = nil
    }

    private func cleanExpiredItems() {
        // Heavy I/O or database cleanup here
    }
}

This is a good example of a timer that:

  • Runs on a dedicated serial queue.
  • Handles background work without blocking the UI.

When people talk about real examples of timer examples with Grand Central Dispatch in Swift, this kind of periodic background maintenance is usually near the top of the list.

Debouncing user input with a GCD timer

Modern apps often debounce text input to avoid hammering APIs. For instance, you might wait 400 ms after the user stops typing before firing a search request.

final class SearchDebouncer {
    private var timer: DispatchSourceTimer?
    private let queue = DispatchQueue(label: "com.example.searchdebounce")

    func scheduleSearch(delay: TimeInterval = 0.4,
                        perform: @escaping () -> Void) {
        timer?.cancel()

        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.schedule(deadline: .now() + delay)

        timer.setEventHandler {
            perform()
        }

        self.timer = timer
        timer.resume()
    }

    func cancel() {
        timer?.cancel()
        timer = nil
    }
}

In UIKit or SwiftUI, you’d call scheduleSearch from your text field change handler. This example of debouncing is one of the best examples of timer examples with Grand Central Dispatch in Swift because it shows how to use a one-shot timer instead of a repeating one.

Rate-limiting network pings or analytics

Another pattern: you want to send analytics or heartbeat pings at most once every N seconds, even if events arrive more frequently.

final class HeartbeatScheduler {
    private var timer: DispatchSourceTimer?
    private let queue = DispatchQueue(label: "com.example.heartbeat")
    private var pending = false

    func scheduleHeartbeat(interval: TimeInterval = 30,
                           send: @escaping () -> Void) {
        pending = true

        if timer == nil {
            let timer = DispatchSource.makeTimerSource(queue: queue)
            timer.schedule(deadline: .now(), repeating: interval)

            timer.setEventHandler { [weak self] in
                guard let self = self else { return }
                guard self.pending else { return }

                self.pending = false
                send()
            }

            self.timer = timer
            timer.resume()
        }
    }

    func stop() {
        timer?.cancel()
        timer = nil
    }
}

Here, multiple calls to scheduleHeartbeat within the interval result in at most one send. These examples include stateful logic layered on top of a simple repeating timer, which is exactly how many production analytics systems behave.

GCD timer with tolerance for better battery life

Apple has long recommended using timer tolerance to help the system coalesce wakeups and save battery. While DispatchSourceTimer doesn’t have a direct tolerance property like Timer, you can approximate tolerance by rounding your schedule times.

For example, if you’re running a background data sync every 5 minutes, you can align to the nearest minute boundary:

extension DispatchSourceTimer {
    static func makeAlignedTimer(interval: TimeInterval,
                                 alignment: TimeInterval,
                                 queue: DispatchQueue,
                                 handler: @escaping () -> Void) -> DispatchSourceTimer {
        let timer = DispatchSource.makeTimerSource(queue: queue)

        let now = Date().timeIntervalSince1970
        let nextAligned = ((now / alignment).rounded(.up)) * alignment
        let delay = nextAligned - now

        timer.schedule(deadline: .now() + delay, repeating: interval)
        timer.setEventHandler(handler: handler)
        return timer
    }
}

// Usage
let queue = DispatchQueue(label: "com.example.sync")
let timer = DispatchSourceTimer.makeAlignedTimer(
    interval: 5 * 60,
    alignment: 60,
    queue: queue
) {
    // Perform sync
}

timer.resume()

While this isn’t a medical or health timer like those discussed on sites such as Mayo Clinic, the same principle applies: reducing unnecessary wakeups improves device “health” by saving battery and reducing CPU churn.

Swift concurrency and GCD timers living side by side

Even with Swift concurrency (async/await, Task), GCD timers are still very relevant in 2024–2025. You can bridge them into async code using continuations or by exposing async streams.

Here’s an example of wrapping a GCD timer as an AsyncSequence of ticks:

struct GCDTimerSequence: AsyncSequence {
    typealias Element = Void

    let interval: TimeInterval

    struct AsyncIterator: AsyncIteratorProtocol {
        private var continuation: AsyncThrowingStream<Void, Error>.Iterator

        init(interval: TimeInterval) {
            let stream = AsyncThrowingStream<Void, Error> { continuation in
                let queue = DispatchQueue(label: "com.example.gcdtimersequence")
                let timer = DispatchSource.makeTimerSource(queue: queue)
                timer.schedule(deadline: .now() + interval, repeating: interval)

                timer.setEventHandler {
                    continuation.yield(())
                }

                continuation.onTermination = { _ in
                    timer.cancel()
                }

                timer.resume()
            }

            self.continuation = stream.makeAsyncIterator()
        }

        mutating func next() async throws -> Void? {
            try await continuation.next()
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(interval: interval)
    }
}

// Usage in an async context
Task {
    for try await _ in GCDTimerSequence(interval: 1.0) {
        print("Tick from async sequence")
    }
}

This is one of the more advanced examples of timer examples with Grand Central Dispatch in Swift, showing how legacy GCD primitives still play nicely with the newer concurrency model.

Handling app backgrounding and invalidation

Real examples always include lifecycle edge cases. If you rely on timers, you have to think about what happens when the app moves to the background or is suspended.

A simple pattern:

final class LifecycleAwareTimer {
    private var timer: DispatchSourceTimer?
    private let queue = DispatchQueue(label: "com.example.lifecycle")

    func start() {
        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.schedule(deadline: .now(), repeating: 2.0)

        timer.setEventHandler {
            print("Doing periodic work")
        }

        self.timer = timer
        timer.resume()

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidEnterBackground),
            name: UIApplication.didEnterBackgroundNotification,
            object: nil
        )

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillEnterForeground),
            name: UIApplication.willEnterForegroundNotification,
            object: nil
        )
    }

    @objc private func appDidEnterBackground() {
        timer?.suspend()
    }

    @objc private func appWillEnterForeground() {
        timer?.resume()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
        timer?.cancel()
    }
}

This example of lifecycle-aware timers shows a common mistake: suspending a timer that’s already suspended will crash. In real apps, you’d track the suspension state to avoid double-suspends or double-resumes.

For background behavior, Apple’s official documentation on Energy Efficiency and background execution strategies is worth reading. While it’s not Swift-specific like these examples of timer examples with Grand Central Dispatch in Swift, it gives a broader picture of how timers affect battery and responsiveness.

If you’re building timers for health or safety use cases (for example, workout intervals, medication reminders, or cooldown periods), you need to think beyond just code:

  • How accurate does the timer need to be?
  • What happens if the app is killed?
  • Do you need notifications or background tasks instead of pure in-memory timers?

Organizations like the National Institutes of Health and CDC publish guidance on adherence, reminders, and behavior patterns. While they won’t teach you GCD, they’re helpful context when you’re deciding whether an in-app timer is enough or if you need scheduled notifications, HealthKit integration, or even server-side scheduling.

In other words, the best examples of timer examples with Grand Central Dispatch in Swift inside health or fitness apps usually combine:

  • A GCD timer while the app is in the foreground.
  • Local notifications or background tasks to cover app suspension.

Patterns and best practices from these examples

Across all of these real examples of timer examples with Grand Central Dispatch in Swift, a few patterns repeat:

Always cancel timers you own

Every example of a GCD timer here has a stop or deinit cleanup path that calls cancel(). If you don’t cancel, the timer will keep its queue alive and may keep your object in memory longer than you expect.

Use the right queue for the job

  • UI updates: main queue.
  • I/O, networking, computation: background queues.

Mixing these leads to stutters or race conditions. In the examples of timer examples with Grand Central Dispatch in Swift above, you’ll notice a consistent separation.

Avoid retain cycles

Using [weak self] in setEventHandler is almost always the right move. Let the owner decide when the timer should die, not the other way around.

Prefer higher intervals when possible

From a performance and battery standpoint, fewer wakeups are better. Apple’s performance docs and general optimization principles (similar in spirit to energy and workload recommendations on sites like Harvard) all push in the same direction: do more work less often, and batch when you can.

FAQ: Timer examples with Grand Central Dispatch in Swift

Q: Can you give another simple example of a one-shot GCD timer in Swift?
Yes. A very small example of a one-shot timer that fires once after 2 seconds:

let queue = DispatchQueue.global(qos: .background)
let timer = DispatchSource.makeTimerSource(queue: queue)

timer.schedule(deadline: .now() + 2.0)

timer.setEventHandler {
    print("Fired once after 2 seconds")
    timer.cancel()
}

timer.resume()

Q: When should I use GCD timers instead of Timer?
Use GCD timers when you need more control over the queue, better behavior in background work, or when you’re already using GCD heavily. Timer is fine for simple UI work, but many of the best examples of timer examples with Grand Central Dispatch in Swift come from scenarios where Timer’s runloop behavior becomes limiting.

Q: Are GCD timers accurate enough for health or safety-critical timing?
They’re good for most consumer use cases, but you should not rely on any in-app timer for medical or life-critical behavior. For that, you’d combine system notifications, background tasks, and server-side scheduling, and you’d follow guidance from health authorities like NIH or CDC around adherence and safety.

Q: Do these examples include support for Swift concurrency out of the box?
Most of the examples are GCD-first, but you saw how to wrap a GCD timer in an AsyncSequence. In modern apps, it’s common to keep GCD timers as the low-level primitive and expose async-friendly APIs on top.

Q: Are there any pitfalls when suspending and resuming GCD timers?
Yes. Calling resume() more times than suspend() (or vice versa) leads to crashes. In production, track the suspension state in a Boolean and guard your calls. Many real examples of timer examples with Grand Central Dispatch in Swift in open source projects include that extra state management for safety.

Explore More Swift Code Snippets

Discover more examples and insights in this category.

View All Swift Code Snippets