Fix: `Timer` Call Produces a Type Error in Swift

Fix: `Timer` Call Produces a Type Error in Swift

Written by

in

Quick answer: Use Timer.scheduledTimer to create and schedule a timer; do not call timer(timeInterval:...) as if it were an instance method. Use repeats: true for recurring work, invalidate the timer when it is no longer needed, and use #selector with an Objective-C-exposed method if you choose the selector-based API.

Why this happens

Timer is a type, not a function you call on an existing timer variable. In the failing code, var timer = NSTimer() creates a timer, but timer(timeInterval:target:selector:userInfo:repeats:) is then written like a function call. That syntax does not create or schedule a timer, so Swift reports a type error.

There is a second issue with the selector argument. A selector must identify a compatible method, using Swift’s #selector syntax. The method must be exposed to Objective-C, commonly with @objc. In modern Swift, the block-based timer API avoids selectors altogether and is often the simplest option.

Creating a timer and scheduling it are separate concepts. Timer.scheduledTimer does both. The Timer(timeInterval:repeats:block:) initializer creates a timer but does not add it to a run loop, so it will not fire until you add it yourself.

How to fix it

1. Schedule a timer with a closure

For most new Swift code, use the block-based API. It requires Foundation, which UIKit-based apps normally import through UIKit. The timer below updates a counter every half second:

import Foundation

final class Counter {
    private var timer: Timer?
    private var count = 0

    func start() {
        stop() // Prevent multiple timers if start() is called again.

        timer = Timer.scheduledTimer(
            withTimeInterval: 0.5,
            repeats: true
        ) { [weak self] timer in
            guard let self else {
                timer.invalidate()
                return
            }

            self.count += 1
            print(self.count)
        }
    }

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

scheduledTimer(withTimeInterval:repeats:block:) schedules the timer on the current run loop. If you call start() from the main thread, the closure runs on that run loop as well. The weak capture prevents the timer’s stored closure from keeping the Counter alive indefinitely. Calling invalidate() stops the timer; a repeating timer does not stop itself after firing.

For a one-time delayed action, set repeats to false:

let timer = Timer.scheduledTimer(
    withTimeInterval: 2.0,
    repeats: false
) { _ in
    print("Two seconds have passed")
}

Keep a reference to a one-shot timer if you might need to cancel it before it fires. Call invalidate() to cancel it.

2. Use a selector when targeting an Objective-C method

The selector-based API is useful when you already have a target method or need to work with an API that uses selectors. The method must be exposed to Objective-C, and the selector must match its signature:

import UIKit

final class ViewController: UIViewController {
    private var timer: Timer?
    private var count = 0

    func startTimer() {
        timer?.invalidate()
        timer = Timer.scheduledTimer(
            timeInterval: 0.5,
            target: self,
            selector: #selector(timerDidFire(_:)),
            userInfo: nil,
            repeats: true
        )
    }

    @objc private func timerDidFire(_ timer: Timer) {
        count += 1
        print(count)
    }

    func stopTimer() {
        timer?.invalidate()
        timer = nil
    }
}

The #selector(timerDidFire(_:)) expression names a method that accepts the firing timer. If your method has no parameter, use a matching no-argument selector such as #selector(timerDidFire) and declare the method without an argument. Do not pass a method reference such as update without #selector to the selector parameter.

Invalidate the timer when the work should stop, such as when a screen no longer needs to update. A repeating timer continues firing until it is invalidated. Starting a new timer without stopping the previous one can leave multiple timers running and updating the same UI.

3. Add an unscheduled timer to a run loop yourself

Use this approach only when you need to control which run loop and mode receive the timer. An initialized timer does not start automatically:

let timer = Timer(timeInterval: 0.5, repeats: true) { _ in
    print("Tick")
}

RunLoop.current.add(timer, forMode: .common)

Store the timer if you need to stop it later, then call invalidate(). For ordinary UI work, Timer.scheduledTimer is less error-prone because it performs the scheduling step for you.

4. Check the run loop and timing expectations

A timer fires through a run loop. A scheduled timer is added to the current run loop in its default mode. If the run loop is busy, or is running in a different mode, a callback can arrive later than its requested interval. A timer is therefore suitable for periodic updates and delayed actions, but not for precise real-time timing.

If the timer is meant to update a UIKit view, create and schedule it on the main thread, and update the view from that callback. If the timer appears not to fire, check that it was scheduled or added to a run loop, that it has not been invalidated, and that its owning object has not gone away.

How to prevent it

  • Create timers with Timer.scheduledTimer unless you specifically need manual run-loop scheduling.
  • Call invalidate() before replacing a timer and when its work is no longer needed.
  • Store a reference to timers you may need to cancel.
  • Use [weak self] in a stored timer closure when the owner also stores the timer.
  • For selector timers, use #selector and expose the target method with @objc.
  • Do not rely on a timer firing at an exact instant; run-loop activity can delay it.

FAQ

Why does my timer never fire?

A timer made with Timer(timeInterval:repeats:block:) is not scheduled automatically. Add it to a run loop, or use Timer.scheduledTimer instead. Also check that the timer was not invalidated and that its run loop is still running.

How do I stop a repeating timer?

Call invalidate() on the timer. Set your stored reference to nil afterward if you no longer need it. Invalidating a timer stops future firings; it does not undo work already performed by a callback.

Should I use a timer for an exact countdown?

A timer callback can be delayed, so counting callbacks is not reliable for an exact elapsed duration. For a countdown display, keep a start or end Date and calculate the remaining time when the timer updates the display. Use the timer to refresh the display, not as the authoritative clock.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.