Scheduling Work in Swift using `NSTimer`
NSTimer can be used to perform scheduled work in Swift. Sometimes it is simpler than other options like GCD (Grand Central Dispatch) or NSOperationQueue, particularly if what you are scheduling needs to run on the main UI thread to access UIView instances anyway. There are also more options coming in the future such as Actors.
Here is an example of using NSTimer running a block of code five times, one second apart:
import Foundation import PlaygroundSupport import UIKit let maxRepeats = 5 var currentRepeats = 0 let blockTimerWith5Runs = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in currentRepeats += 1 print("Block ran \(currentRepeats) times(s)!") if (currentRepeats >= maxRepeats) { timer.invalidate() } }
In an iOS app, RunLoop.main will already be running. In XCode playgrounds you also need:
RunLoop.main.run(until: Date(timeIntervalSinceNow: 6))
Output:
Block ran 1 times(s)! Block ran 2 times(s)! Block ran 3 times(s)! Block ran 4 times(s)! Block ran 5 times(s)!
It can also be used to run a function:
class FunctionTimerExample { init() { Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.peep), userInfo: nil, repeats: false) } @objc func peep() { print("Function ran once!") } } let functionTimerExample = FunctionTimerExample() RunLoop.main.run(until: Date(timeIntervalSinceNow: 2))
Output:
Function ran once!
The function can optionally receive the Timer itself as an argument with some user info attached:
class Counter { var count : Int init(_ count: Int) { self.count = count } } class FunctionTimerWithArgExample { init() { let userInfo = Counter(0) Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.peepArg), userInfo: userInfo, repeats: true) } @objc func peepArg(timer: Timer) { guard let userInfo = timer.userInfo as? Counter else { return } userInfo.count += 1 print("Function with arg ran \(userInfo.count) time(s)!") if (userInfo.count >= maxRepeats) { timer.invalidate() } } } let functionTimerWithArgExample = FunctionTimerWithArgExample() RunLoop.main.run(until: Date(timeIntervalSinceNow: 6))
Output:
Function with arg ran 1 time(s)! Function with arg ran 2 time(s)! Function with arg ran 3 time(s)! Function with arg ran 4 time(s)! Function with arg ran 5 time(s)!
An example use case is debouncing a search input
class DebouncedSearchViewController: UIViewController { var textField = UITextField(frame: CGRect(x: 20, y: 20, width: 200, height: 24)) var timer : Timer? override func viewDidLoad() { super.viewDidLoad() view.addSubview(textField) textField.placeholder = "Enter search" textField.backgroundColor = .green self.textField.addTarget( self, action: #selector(self.textFieldDidChange(textField:)), for: .editingChanged); } @objc func textFieldDidChange(textField: UITextField){ print("Text changed: " + textField.text!) timer?.invalidate() timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { _ in guard let text = textField.text else { return } print("Submit debounced search query for: \(text)") }) } } let vc = DebouncedSearchViewController() vc.view.frame = CGRect(x: 0, y: 0, width: 300, height: 300) PlaygroundPage.current.needsIndefiniteExecution = true PlaygroundPage.current.liveView = vc.view
Demo of typing `ABC` quickly followed by `Z` after waiting for a second:
At one company I worked at, we found having a 500ms debounce instead of 200ms reduced the number of network calls, and thus reduced operational costs, without impacting user engagement. So there are debounce values you can use that will save things like cost, network bandwidth, and battery life without hurting user experience.
Warning: If doing some blocking operation like networking or file IO, make sure to start the timer off the main thread, or create a different run loop and add it to that run loop manually. Everything above runs on the main UI thread, so could lock up the user interface on the user if blocking operations were added.












