3 mistakes to avoid with Closures


You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿


I want to tell you about 3 mistakes you want to avoid when working with closures in Swift!

#01 - Capturing a variable

Take a look at this code: I create a local variable that contains an integer, and then I define a closure that uses this local variable.

After that, I change the value of the local variable and finally I call the closure.

You might expect this code to print "2".

However this code will actually print "3" 🤨

The reason is that the closure captured the entire local variable and so any change to that variable outside the closure will also happen inside the closure.

This can be a bit unsettling, because we are used to the fact the when we use a value type, like an integer, if we pass that value to a function or assign it to a new variable, we will be dealing with a copy of the value.

But closures behave differently, because by default they do not capture the value but rather the variable that holds the value.

However, it is possible to ask the closure to capture the value instead of the variable, by using what Swift calls the capture list.

With this change, our closure now captures the value of the variable at the moment that the closure is created.

So now our code will print "2" 👌

#02 – Retain Cycles

Another major pitfall of closures is how easy they can result in a memory leak as soon as we start capturing reference types.

Let’s have a look at this new piece of code: I have a simple ViewController that creates a Timer in order to update its label every second with the current date.

This code might seem harmless given how simple it is, but it will actually result in a memory leak.

Let me explain:

1. The ViewController owns the Timer

2. Then, the Timer owns the closure that it will call every second

3. Finally, that closure captures self and so it also owns a reference to the ViewController

And this is what we call a retain cycle!

This means that neither of these references will ever be released from memory, because they will be holding a strong reference to one another forever.

To solve this issue, we need a way to break the retain cycle.

We can do so by instructing the closure to capture a weak reference to self.

Now the closure no longer owns the ViewController.

To account for this, inside the closure self has now become an optional, because it will be nil if the ViewController is deallocated by the time the closure is called.

So if we want to have a non-optional self inside the closure, we need to add an extra line of code to safely unwrap the optional.

#03 - Escaping and non-escaping closures

Now let’s have a look at this new piece of code.

I’m calling the method .map() over an array, and I’m providing a closure that implements the transform that the method .map() will apply to all the elements of the array.

However, this closure is actually different from all the closures we’ve used up until now!

To understand where the difference lies, let’s take a look at the signature of both the method .map() and of the Timer initializer I’ve used just before.

We can notice that, in the case of the Timer, the closure is annotated with @escaping, whereas this is not the case for .map()

And this difference is super important!

Because closures that are annotated with @escaping are allowed to be kept in memory even after that the function we passed them to has returned.

And indeed that’s what happened when we created our Timer: the closure that would be called every second was still in memory after that our Timer had been initialized.

However, closures that are not annotated with @escaping, like the one we pass to .map(), cannot stay in memory after the function we passed them to has returned.

And the compiler will make sure that this rule has indeed been respected.

This is significant, because if we know that a closure has a lifespan that’s limited to the call of a method, then it’s impossible for this closure to create a retain cycle!

And so we can greatly simplify the code of the closure we pass to .map(), because since the closure in non-escaping, we can capture self safely inside it without any fear of creating a retain cycle.

Conclusion

That’s it, these were the 3 mistakes you definitely want to avoid when using closures in Swift!

I hope you’ve enjoyed this article and that it will help you avoid dealing with some frustrating bugs!


You’ve enjoyed this article and you want to support my content creation?

You can leave me a tip ☺️ 👇

Buy Me A Coffee

Here’s the code it you want to experiment with it!

import Foundation

// #01 - Capturing a Variable

var someInteger = 2

let closure = { [someInteger] in
    print(someInteger)
}

someInteger = 3

closure() // prints "2"

// #02 – Retain Cycles

import UIKit

class ViewController: UIViewController {
    var timer: Timer?
    let label = UILabel()
    let formatter = DateFormatter()

    override func viewDidLoad() {
        super.viewDidLoad()

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

            let now = Date()
            self.label.text = self.formatter.string(from: now)
        }
    }
}

// 03 - Escaping and non-escaping closures

extension ViewController {
    func decorateTimeWithEmojis() -> [String] {
        ["⏲️", "⏰", "⏳"].map { emoji in            
            let now = Date()
            return "\(emoji) \(self.formatter.string(from: now))"
        }
    }
}
Previous
Previous

I learned so much from this website 🤯

Next
Next

Are private properties really private? 🤨