When do you really need to use [weak self]? 🤨

Hi 👋

Last Friday was a national holiday in France, which allowed me to enjoy a well-deserved 3-day week-end 😌

Thanks to this extra free time, I managed to record a few new videos that I’m pretty excited about and I also got an idea for a new livestream format.

So stay tuned: I plan on releasing some cool content over the summer 😀


In today’s email, we’re going to talk about a tricky question that iOS developers are bound to ask themselves regularly:

“Should I use [weak self] in this closure?”

To answer this question, we’ll go over a few examples. And as you’ll see, the answer is not always as clear cut as it seems!

For the purpose of these examples, we’ll be using this simple ViewModel:

As indicated, you can consider that the code of the examples has been written inside the init() of the ViewModel.

#01 – @escaping closures with an infinite lifetime

This first example is probably the one that’s easiest to answer if you’re an experienced iOS developer:

The situation is pretty clear cut: we’re dealing with an escaping closure, that will be stored inside the property handler for as long as the ViewModel stays in memory.

This is the typical situation where you definitely want to use [weak self] to avoid a retain cycle:

#02 – non-escaping closures

For this second example, we’re still dealing with a closure, but this time it’s a different kind of closure:

What’s important to notice here is that the closure we pass to .map() isn’t marked as @escaping.

This is important, because it means that the compiler will make sure the closure doesn’t outlive the call to .map().

Because of that, it’s impossible for this closure to create a retain cycle, and so there’s no need to use [weak self] inside of it:

You can notice that the compiler is aware of this and doesn’t force us to explicitly capture self when calling the method .format().

#03 – @escaping closures with a finite lifetime

This new example is a little bit trickier:

This time the closure is indeed marked as @escaping. However, it’s not being stored forever: it’s being stored until it gets executed, and then it will be deallocated.

In this situation, most of the time you can capture self without creating a retain cycle, provided that the closure isn’t scheduled to be executed at a very far away time.

However, you could also decide to use [weak self] inside this closure.

What’s important to understand is that both options will result in a different behavior of your code:

  • if you capture self, then you will make sure that the instance is still in memory when the closure gets executed: this might be the behavior that you want to implement.

  • if you use [weak self], then the instance might no longer be in memory when the closure gets executed: once again, this might totally be a behavior that you are okay with.

It’s also important to understand that in this situation you probably don’t want to be using [unowned self], as it would have very high chances to result in crash!

#04 – Closures passed to a Task

For this last example, let’s consider code that uses Swift Concurrency:

This example is particularly tricky because, as you can see, the compiler isn’t forcing us to explicitly capture self.

So we might be tempted to assume that we don’t need to use [weak self] here, but that would be a mistake!

Because it’s important to realize that this Task is observing an AsyncSequence that never ends: this means that the closure will always remain in memory, and so the instance will never be deallocated.

In this situation, not only do you need to be using [weak self], but you probably also want to check if self is still in memory, in order to stop the Task once it has been deallocated:

You also want to make sure that you are unwrapping self inside the for loop!

Because if you unwrap it outside the loop, then the closure will always be retaining a strong reference to self, and it will negate the effect of having used [weak self] 🙃

Conclusion

I hope you’ve enjoyed this little overview of the situations where it makes sense, or not, to use [weak self] inside a closure!

As we’ve seen the answer is far from always being clear cut, and the latest additions to Swift have even managed to add new tricky edge cases.

Here’s the code if you want to experiment with it:

import Foundation
import UIKit

class ViewModel {

    func format(_ value: Int) -> String {
        return "\(value)"
    }

    var handler: ((Int) -> Void)? = nil

    init() {

        // 1st example -> [weak self] is needed
        handler = { [weak self] value in
            let formatted = self?.format(value)
            print(formatted as Any)
        }

        // 2nd example -> no [weak self] is needed
        let formatted = [1, 2, 3].map { value in
            return format(value)
        }
        print(formatted)

        // 3rd example -> [weak self] is not mandatory,
        //                but whether we use it changes the behavior
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            let formatted = self?.format(42)
            print(formatted as Any)
        }

        // 4th example -> [weak self] is needed
        Task { [weak self] in
            let asyncSequence = await NotificationCenter
                .default
                .notifications(
                    named: UIDevice.orientationDidChangeNotification
                )

            for await _ in asyncSequence {
                guard let self else { return }

                let formatted = format(42)
                print(formatted as Any)
            }
        }
    }
}

That’s all for this email, thanks for reading it!

If you’ve enjoyed it, feel free to forward it
to your friends and colleagues 🙌

I wish you an amazing week!

❤️

Previous
Previous

Discover async let

Next
Next

Bad practice: testing if a String is empty