Weak Self in Swift Made Easy: What it is and why it’s needed

When developing iOS apps in Swift, you are bound, sooner or later, to encounter weak self references. That’s especially true in the callbacks of network requests.

At first, weak self references might seem puzzling and, sometimes, annoying. In this article, we will see why they are needed and how you can fix them in your code.



Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Manual and automatic memory management

To understand what weak references are, you need to have a basic understanding of memory management in Swift.

Any object you create in your apps takes some space in memory. The operating system marks that space as used so that no other object can overwrite its data.

Memory is limited and expensive, especially on iOS devices. You can’t just keep adding objects, or you will soon run out of memory. You need to free the space taken by objects you don’t need anymore (well, unless your memory is going to explode soon).

That’s easier said than done.

Memory management has been a massive headache for developers in any language. Initially, languages like C and C++ relied on manual memory management. They passed the programmer the burden of writing code to reserve and free memory for data structures.

That was both cumbersome and error-prone. It was easy to make mistakes, freeing memory that was still needed, leading to crashes. It was also easy to forget to free memory that was no longer required before losing a reference to it, causing memory leaks that eventually filled up all the available space.

Fed up with all that, Lisp introduced garbage collection, an automatic memory management mechanism that was later made popular by Java. In it, a collector periodically scans the memory to check for objects that are no longer in use.

automatic memory management with garbage collection

Automatic reference counting (ARC)

Unfortunately, garbage collection is not free. The system needs to briefly stop your app’s execution to let the collector check the memory. That leads to lags in the interface, which Apple decided was not acceptable.

So, when they released iOS, they went back to a manual mechanism called reference counting. In it, every time you keep a reference to an object, you retain it, which increases its reference count by one. When you don’t need an object anymore, you release it, which decreases its counter by one.

So, the reference count just keeps track of how many references an object has at any moment. When that gets to zero, it means the object is not needed anymore, and the system frees its memory.

While better than managing memory directly, this is still a manual process, so it’s also error-prone. The developer needs to write code to retain and release objects explicitly. That makes it possible, again, to crash your app or leak memory.

Through experience, developers created a series of best practices that dictated where to put retain and release code. Apple took those rules and embedded them in the compiler, creating automatic reference counting, or ARC.

The compiler now inserts the reference counting code in your program for you.

ARC can still leak memory if you create strong reference cycles

I used manual memory management in all its forms, and ARC is a vast improvement over it. Unfortunately, it still has problems.

In a reference counting system, be it manual or automatic, we can still leak memory by creating retain cycles.

As the name suggests, when two or more objects circularly point to each other, each object increases the reference count of the next one. That means that no count will ever get to zero, and all the objects in the cycle will stay in memory indefinitely.

Let’s look at an example.

class Company {
	let name: String
	var ceo: Person?
	
	init(name: String) {
		self.name = name
	}
}

class Person {
	let name: String
	var company: Company?
	
	init(name: String) {
		self.name = name
	}
}

With these two classes, it’s easy to create a reference cycle by design.

var john: Person? = Person(name: "John Doe")
var acme: Company? = Company(name: "Acme Corporation")

a reference cycle between two objects in Swift

When later we won’t need the two objects anymore, and we remove their references, ARC will keep them in memory because their count can’t reach zero.

The solution is to break the cycle by declaring one of the references as weak. Weak references do not increase the reference count of an object, thus solving the problem.

Since a weak reference does not keep an object in memory, the referenced object can disappear at any moment. For that reason, weak references must be optional. When the referenced object goes away, the compiler sets any weak reference pointing to it to nil.

Self references and reference cycles in Swift closures

There is a particular type of reference cycle, very common in Swift code, that happens in escaping closures.

Swift closures are often used as callbacks in network requests, timers, dispatch queues, and other asynchronous tasks. Frameworks like SwiftUI and Combine also rely heavily on closures.

By definition, closures capture their context. That means that a closure keeps a reference to any object referenced in its body.

The crucial point is that, in Swift, closures are not values, but reference types like objects. This means that closures can also create reference cycles.

One standard reference captured by closures is self.

Whenever you access the properties or the methods of an object, there is an implicit reference to self. If you do that inside a closure, the closure will capture that self reference.

An example will make it clearer.

Let assume we are writing an app that tracks time (either a stopwatch or time tracking app). The user can to start and stop a timer, and the app displays the elapsed time on screen.

For that, we need a class that keeps track of time.

class TimeTracker {
	var elapsedSeconds: Int = 0
	var timer: Timer?
	
	func start() {
		timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
			self.elapsedSeconds += 1
		}
	}
}

The Timer class uses an escaping closure for its callback, where we increase the value of elapsedSeconds.

As you can see, in that closure, we have a reference to self. The Swift compiler forces you to make explicit any self reference in a closure so that it’s clear that capturing is happening.

That creates a reference cycle:

  • The TimeTracker keeps a reference to the Timer in its timer property.
  • The Timer keeps a reference to the closure to execute it every time it fires.
  • The closure keeps a reference to the TimeTracker to increase its elapsedSeconds property.

Breaking reference cycles in callbacks with weak self references

To break the reference cycle, you might think that it’s enough to set the timer property to nil when the user stops the timer.

class TimeTracker {
	var elapsedSeconds: Int = 0
	var timer: Timer?
	
	func start() {
		timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
			self.elapsedSeconds += 1
		}
	}
	
	func stop() {
		timer?.invalidate()
		timer = nil
	}
}

But you would be wrong.

In the TimeTracker code, you can’t make assumptions about any external code.

A caller would not see any reference cycle and might dispose of a TimeTracker instance by merely setting a reference to nil. But since that object is part of a reference cycle, it would leak memory.

To break the cycle, then we have to weaken one of the references in the sequence. But:

  • We need to keep the timer in memory to use it, so the timer property can’t be weak.
  • The timer also needs a strong reference to the closure to keep that in memory.

So, the only reference left is the self reference in the closure, which we can weaken using a capture list:

class TimeTracker {
	var elapsedSeconds: Int = 0
	var timer: Timer?
	
	func start() {
		timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
			self?.elapsedSeconds += 1
		}
	}
	
	func stop() {
		timer?.invalidate()
		timer = nil
	}
}

Avoiding optional unwrapping and unowned references

Many developers gripe about the fact that weak references must be optional. In the code above, you can see the ? operator after the self keyword in the closure.

Unwrapping that optional can becomes tedious if you need to reference self many times in a closure. My advice is to move that code to a separate method, where the self reference will be implicit. You can then can call that method in the closure, thus having to unwrap self only once.

But if you want to keep all code in the closure, there is an alternative.

class TimeTracker {
	var elapsedSeconds: Int = 0
	var timer: Timer?
	
	func start() {
		timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
			guard let `self` = self else { return }
			self.elapsedSeconds += 1
		}
	}
	
	func stop() {
		timer?.invalidate()
		timer = nil
	}
}

The guard statement unwraps the self reference and assigns it to a let constant with the same name (in Swift, you can reuse some keywords as variable names using backticks). The following code then uses that constant, looking the same but without the unwrapping.

There is also another alternative, which I don’t recommend.

Swift also offers the unowned keyword, which weakens a reference without making it optional. So, no more optional unwrapping.

But that also introduces potential crashes. If you try to access an unowned reference that points to an object that does not exist anymore, your app will crash.

That happens in our example too. In iOS, timers are kept in memory by the app’s run loop. When the TimeTracker instance goes away, the timer does not. When it fires, and the closure tries to increase the elapsedSeconds property, the app crashes.

So, as a general rule, you should use unowned only when you are sure that the referenced object will stay in memory as long as the referencing one. Which, in practice, rarely happens, so it’s better to use weak references to remain safe.

Architecting SwiftUI apps with MVC and MVVM

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

5 thoughts on “Weak Self in Swift Made Easy: What it is and why it’s needed”

  1. this is a great article! more diagrams with boxes and arrows would definitely make it even better…
    still, that’s true for everything

    Reply
  2. Is it possible using weak for timer?

    class TimeTracker {
    var elapsedSeconds: Int = 0
    weak var timer: Timer? // weak timer

    func start() {
    timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
    guard let `self` = self else { return }
    self.elapsedSeconds += 1
    }
    }

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

    Reply
    • Yes, that works, but for a hidden reason.

      Using the scheduledTimer(withTimeInterval:repeats:) method schedules a timer on a run loop, which keeps a strong reference to the timer. That’s why the timer does not disappear even if you keep a weak reference to it.

      Reply

Leave a Comment