Swift
programming
scheduling
automation
coding-tutorial

Do something every x minutes in Swift

Master System Design with Codemia

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

Introduction

Running code every few minutes in Swift is straightforward while your app is active, but the right tool depends on where the work runs and how exact the schedule needs to be. The main distinction is between ordinary in-process timers and true background execution managed by iOS.

Use Timer for Run-Loop-Based Work

If the task belongs to an active screen, view model, or other main-thread object, Timer is the simplest option. It integrates with the run loop and is a good fit for UI refresh, lightweight polling, or periodic state updates while the app is in the foreground.

swift
1import Foundation
2
3final class Poller {
4    private var timer: Timer?
5
6    func start() {
7        stop()
8
9        timer = Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { [weak self] _ in
10            self?.refresh()
11        }
12    }
13
14    func stop() {
15        timer?.invalidate()
16        timer = nil
17    }
18
19    private func refresh() {
20        print("Refreshing at \(Date())")
21    }
22}

This approach works well when "every five minutes" means "while this object is alive and the app is running." It does not promise exact wall-clock precision. Run-loop activity, device load, and app lifecycle transitions can shift the callback slightly.

That is usually fine for UI refresh or soft polling. It is not fine for compliance-grade scheduling, alarms, or work that must happen at an exact real-world time.

Use DispatchSourceTimer for Queue Control

When the periodic task should run on a specific dispatch queue rather than the main run loop, DispatchSourceTimer gives you more control. That is useful for file processing, background parsing, or network preparation that should not block UI work.

swift
1import Foundation
2
3final class BackgroundWorker {
4    private let queue = DispatchQueue(label: "com.example.worker")
5    private var timer: DispatchSourceTimer?
6
7    func start() {
8        stop()
9
10        let source = DispatchSource.makeTimerSource(queue: queue)
11        source.schedule(deadline: .now(), repeating: .seconds(120), leeway: .seconds(5))
12        source.setEventHandler {
13            print("Background queue tick at \(Date())")
14        }
15        source.resume()
16        timer = source
17    }
18
19    func stop() {
20        timer?.cancel()
21        timer = nil
22    }
23}

The leeway parameter is worth paying attention to. It tells the system how much flexibility it has when scheduling the wakeup, which can improve battery efficiency. For most polling jobs, a small amount of leeway is a better tradeoff than chasing perfect timing.

Manage Timer Ownership Explicitly

The most common bug is not the timer API itself, but ownership. Repeating timers continue until they are invalidated or cancelled. If a screen appears twice and each appearance creates a new timer, the task may run in duplicate. If the closure strongly captures its owner, you can also create a retain cycle.

That is why timer setup should be paired with deliberate cleanup. In UIKit, that often means starting and stopping in lifecycle methods. In SwiftUI, it usually means placing the timer inside a model object rather than scattering timer creation across view modifiers.

swift
1import Foundation
2import Observation
3
4@Observable
5final class ClockModel {
6    private var timer: Timer?
7    private(set) var tickCount = 0
8
9    func start() {
10        guard timer == nil else { return }
11
12        timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
13            self?.tickCount += 1
14        }
15    }
16
17    func stop() {
18        timer?.invalidate()
19        timer = nil
20    }
21}

This pattern keeps the scheduling logic in one place and makes repeated view rendering less dangerous.

iOS Background Limits Matter More Than the API Choice

Many developers ask how to run code every x minutes when the real requirement is background refresh. A normal Timer or DispatchSourceTimer cannot keep firing after the app is suspended. Once iOS suspends your process, your timer stops because your code is no longer running.

If the actual goal is background work, use a background-specific mechanism:

  • 'BGAppRefreshTask for lightweight refresh opportunities.'
  • 'BGProcessingTask for deferred heavier work.'
  • Push notifications when the server should trigger client activity.
  • Server-side scheduling if the task does not need to live on the device.

That architectural distinction matters more than any specific timer call. A correctly written repeating timer is still not a background scheduler.

Design the Interval Strategy, Not Just the Interval

In production, the question is rarely only "how do I fire every five minutes." You also need to decide what happens when the previous run has not finished, when the network is down, or when thousands of devices poll simultaneously. Good periodic design often adds jitter, backoff, and overlap protection so the timer does not become a source of load spikes or duplicated work.

That design layer is what separates a demo timer from a production polling loop.

Common Pitfalls

  • Expecting a timer to keep running while the iOS app is suspended.
  • Starting multiple repeating timers for the same job.
  • Doing heavy synchronous work inside a main-thread timer callback.
  • Forgetting to invalidate or cancel the timer when the owner goes away.
  • Treating a local timer as a substitute for proper background APIs.

Summary

  • Use Timer for periodic foreground work tied to the run loop.
  • Use DispatchSourceTimer when queue control matters.
  • Pair timer startup with explicit cleanup and avoid duplicate scheduling.
  • Do not rely on timers for guaranteed background execution on iOS.
  • Add backoff, jitter, and overlap control for real production polling.

Course illustration
Course illustration

All Rights Reserved.