Simplifying Swift Timers: solving memory leaks & complexity once and for all

Creating easy-to-use Timers with automatic memory management

Oleg Dreyman
6 min readJul 17, 2023

TL;DR? Explaining how to create a system for intuitive Swift Timers with automatic memory management. Minimal gist here, fully featured Swift package here, or read the article for details

This article is not an explainer on Timer API. If you need a deep dive on Foundation.Timer in Swift, I recommend The ultimate guide to Timer by Paul Hudson

Swift’s Timer (ex-NSTimer) is a very powerful API, but can be quite unintuitive to use. My main issues with it are:

  1. Vagueness about memory management: there are many ways to create and manage Timer instances, and all of them have different rules about when and how they are deallocated. Developers must always stay vigilant, as this complexity can lead to memory leaks and other bugs.
  2. API complexity: after figuring out memory management, you are still presented with many options to get right: use Timer.init or Timer.scheduledTimer? Which RunLoop to use? What on earth are run loop modes? Developers often need to go searching for documentation instead of relying on the API intuitively. For the vast majority of use cases, this is simply unnecessary.

Solution: “Timers“ class

The initial idea is to create a Timers class that will hold references to timers and invalidate them (thus deallocating) once this object itself is deallocated. Here’s the core structure of it:

public final class Timers {

private var timers: [Foundation.Timer] = []

public init() { }

public func clear() {
for timer in timers {
timer.invalidate()
}
timers = []
}

deinit {
clear()
}

// to be continued...
}

Hopefully, the idea is clear. Whoever “owns” the Timers instance (for example, a view controller) controls when the timers are stopped and deallocated. No need to manually juggle timer instances: when the owner gets deallocated, Timers gets deallocated, which invalidates and deallocates all the timers. In the end, the usage will look like this:

final class ExampleViewController: UIViewController {

// will invalidate & deallocate all the timers
// when ExampleViewController deallocates
let timers = Timers()

override func viewDidLoad() {
super.viewDidLoad()

timers.addTimer(/*...*/) // we'll get into specifics next
}
}

Adding helper Timer functions

Our goal is to create an easy-to-use, intuitive API for Timers while also giving our API users full power of Foundation’s Timer. So first we’ll create one “core” addTimer function that will enable us to make memory-safe timers easily, and then we can add convenience functions that are more than enough for common use cases.

The “core” function looks like this:

 extension Timers {
public func addTimerManually(
runLoop: RunLoop = .main,
runLoopMode: RunLoop.Mode = .common,
timer: Foundation.Timer
) {
runLoop.add(timer, forMode: runLoopMode)
timers.append(timer)
}
}

Users can still use custom run loop and run loop modes for those more sophisticated scenarios, but this function provides reasonable default values (RunLoop.main and RunLoop.Mode.common) for standard use cases.

This already gives us the benefit of simplified automatic memory management, since the timer added this way will get automatically invalidated and deallocated when the Timers instance gets deallocated.

And now it’s time to introduce out first helper function, which goes one step further:

extension Timers {
public func addRepeating<TargetType: AnyObject>(
timeInterval: TimeInterval,
tolerance: TimeInterval = 0,
withTarget target: TargetType,
_ handler: @escaping (TargetType, Timer) -> Void
) {
let newTimer = Timer(timeInterval: timeInterval, repeats: true) { [weak target] timer in
if let target {
handler(target, timer)
}
}
newTimer.tolerance = tolerance
self.addTimerManually(timer: newTimer)
}
}

We’re using TargetType pattern here to avoid relying on our users typing [weak self] every time to avoid retain cycles. Instead, we are the one who are writing [weak target]. This might look strange to you, but it’s actually a pattern that is used by Apple itself. You can read more about the core idea of this pattern in my article No more [weak self], or the weird new future of delegation.

As such, creating a repeating timer now becomes easy and safe:

final class ExampleViewController: UIViewController {

let timers = Timers()

override func viewDidLoad() {
super.viewDidLoad()

timers.addRepeating(timeInterval: 1, withTarget: self) { (self, timer) in
self.reloadData()
}
}

// ...
}

Adding more helper functions

From here, the Timers class is infinitely extendable.

First, we can write a version of addRepeating where you can simply use (self) instead of (self, timer):

extension Timers {
public func addRepeating<TargetType: AnyObject>(
timeInterval: TimeInterval,
tolerance: TimeInterval = 0,
withTarget target: TargetType,
handler: @escaping (TargetType) -> Void
) {
self.addRepeating(
timeInterval: timeInterval,
tolerance: tolerance,
withTarget: target,
handler: { target, _ in handler(target) }
)
}
}
final class ExampleViewController: UIViewController {

let timers = Timers()

override func viewDidLoad() {
super.viewDidLoad()

timers.addRepeating(timeInterval: 1, withTarget: self) { (self) in
self.reloadData()
}
}

// ...
}

This is optional but nice to have, as usually we don’t need direct access to Timer instances in the block.

Then, we can write additional helpers to support other use cases, for example, this one that fires at a specific time and then repeats with a set interval:

extension Timers {
public func addRepeating<TargetType: AnyObject>(
initiallyFireAt fireAt: Date,
thenRepeatWithInterval timeInterval: TimeInterval,
tolerance: TimeInterval = 0,
withTarget target: TargetType,
handler: @escaping (TargetType, Timer) -> Void
) {
let newTimer = Timer(
fire: fireAt,
interval: timeInterval,
repeats: true,
block: { [weak target] timer in
if let target {
handler(target, timer)
}
}
)
newTimer.tolerance = tolerance
self.addTimerManually(timer: newTimer)
}
}

// usage:
timers.addRepeating(
initiallyFireAt: .now.addingTimeInterval(10),
thenRepeatWithInterval: 5,
withTarget: self
) { (self, timer) in
self.reloadData()
}

Or a timer that fires once:

extension Timers {
public func fireAt<TargetType: AnyObject>(
_ fireAt: Date,
withTarget target: TargetType,
handler: @escaping (TargetType) -> Void
) {
let newTimer = Timer(
fire: fireAt,
interval: 0,
repeats: false,
block: { [weak target] _ in
if let target {
handler(target)
}
}
)
addTimerManually(timer: newTimer)
}
}

// usage:
timers.fireAt(date, withTarget: self) { (self) in
self.reloadData()
}

It’s straightforward to add your own helper functions to suit your specific needs — even the more complex ones — without sacrificing simplicity and memory-leak safety. Feel free to do so

Final code

For easy copy-pasting, here’s our final Timers class with one helper function:

public final class Timers {

private var timers: [Foundation.Timer] = []

public init() { }

public func clear() {
for timer in timers {
timer.invalidate()
}
timers = []
}

deinit {
clear()
}

public func addTimerManually(
runLoop: RunLoop = .main,
runLoopMode: RunLoop.Mode = .common,
timer: Timer
) {
runLoop.add(timer, forMode: runLoopMode)
timers.append(timer)
}

public func addRepeating<TargetType: AnyObject>(
timeInterval: TimeInterval,
tolerance: TimeInterval = 0,
withTarget target: TargetType,
handler: @escaping (TargetType, Timer) -> Void
) {
let newTimer = Timer(timeInterval: timeInterval, repeats: true) { [weak target] timer in
if let target {
handler(target, timer)
}
}
newTimer.tolerance = tolerance
addTimerManually(timer: newTimer)
}
}

An extended version is also available as a Swift package:

It has more helper functions and an additional generic function to reduce code repetition and help you write your own helpers more easily.

Hopefully, this proves helpful. In my personal experience, I have found it much more enjoyable to use the Timers class instead of creating NSTimers directly. It also helps knowing my code is much safer as a result.

Thanks for reading the post! Don’t hesitate to ask or suggest anything in the “responses” section below. You can also contact me on Twitter or find me on GitHub. If you’ve written an article — or stumbled upon one — exploring a similar topic, be sure to post a link to it in the responses, and I’ll include it below.

Hi! If you want to support me, please check out my apps: “Ask Yourself Everyday” and “Time and Again”. For business inquiries, please reach out to me at oleg@dreyman.dev. Thanks for reading!

🇺🇦 Donate to Ukraine here

--

--

Oleg Dreyman

iOS development know-it-all. Talk to me about Swift, coffee, photography & motorsports.