Escaping Closures in Swift Explained

Written by LearnAppMaking on March 12 2021 in App Development, Swift

Escaping Closures in Swift Explained

A closure is said to “escape” a function when it’s called after that function returns. In Swift, closures are non-escaping by default. What are escaping closures, anyway? And what do you use @escaping for? Let’s find out!

This tutorial discusses closures, escaping vs. non-escaping, capture lists and retain cycles, and what that means for practical iOS development.

What we’ll get into:

  • What escaping closures are, and why you need them
  • How and when to use the @escaping keyword
  • Risks of escaping closures, such as strong reference cycles
  • Fixing a strong reference cycle in an escaping closure

Enjoy!

  1. Quick Recap: What’s a Closure?
  2. Escaping Closures in Swift
  3. Why Use @Escaping?
  4. Strong Reference Cycles in Escaping Closures
  5. Wrapping Up

Quick Recap: What’s a Closure?

Closures are awesome! You probably already know this, but a closure is a block of code you can pass around and use elsewhere in your code.

It’s kinda like a function you can assign to a variable. You can move the closure around in your code, and call it at a later point. This makes closures exceptionally useful as completion handlers, i.e. the code you call when a long-running task has finished.

Check this out:

let makeGreeting: (String) -> String = { name in
    return "Hello, \(name)!"
}

let message = makeGreeting("Bob")
print(message)
// Output: Hello, Bob!

In the example above the constant makeGreeting contains a closure. It has one parameter of type String, and it’s returning a value of String as well. It works the same way as a function.

When the closure is called in the above example, with makeGreeting("Bob"), its value is assigned to constant message and printed out.

A common use-case for a closure is a so-called completion handler. Say you’re downloading an image from the web that you want to display in a view controller. Your code could look something like this:

let task = URLSession.shared.dataTask(with: URL(string: "http://example.com/cats.jpg")!, completionHandler: { data, response, error in
    // Do something with image data...
})

In the above code, we’re starting a data task with URLSession; making a HTTP request to download some cats.jpg file. The argument for the completionHandler parameter of the dataTask() function is a closure. It’s called when the image has been downloaded, hence the name completion handler.

Closures are powerful. In the example above, we’re writing the response to the data task in the same “place” in the code as the initial request. That means we can start the request and handle it, as if both actions follow each other immediately.

There’s a bit of time between the start of the request and its completion, but we can code that as if it’s immediate. That makes reading the code and reasoning about it much easier!

The code between the squiggly brackets { ··· } is passed into the dataTask(with:completionHandler:) function. Passing around closures is a core principle, and it’s what escaping closures are about, too. Closures that outlive the context that declared them can potentially cause a memory leak, as you’ll soon see!

Good to know: Closures are reference types. Pass ’em into a function and you’re sharing a reference to the closure; it’s not copied.

Escaping Closures in Swift

Let’s take a look at the declaration of that dataTask(with:completionHandler:) function, as found in the Swift Foundation framework’s source code:

func dataTask(with url: URL, 
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

Right there, you can see that this function has 2 parameters:

  1. url of type URL – the URL resource we’re going to download/request
  2. completionHandler – a closure

You can also see that the closure itself has 3 parameters, and returns Void.

  1. Data? – the data that the request itself returns (i.e., the image)
  2. URLResponse? – an object that contains information about the response
  3. Error? – an optional value that contains an error object or nil

The function declaration also contains a special keyword: @escaping. This means that the closure that’s passed in as an argument for this parameter can escape the dataTask(with:completionHandler:) function. In other words, the closure is called after the function dataTask(···) finishes executing.

Here’s the official definition of an escaping closure:

A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns.

It can be helpful to think of an escaping closure as code that outlives the function it was passed into. Think of that function as a jail, and the closure as a bandit who’s escaping the jail.

An example of an escaping closure is the completion handler. Depending on its implementation, a completion handler is typically executed in the future. A length task completes long after it was initiated, which means that the closure escapes and outlives the function it was created in.

If we’ve got escaping closures, we can also have non-escaping closures:

  • An escaping closure is a closure that’s called after the function it was passed to returns. In other words, it outlives the function it was passed to.
  • A non-escaping closure is a closure that’s called within the function it was passed into, i.e. before it returns. This closure never passes the bounds of the function it was passed into.

In Swift, a closure is non-escaping by default. If a closure can escape the function, you’ll need to annotate its function parameter with the @escaping keyword. This is what happens in the code at the top of this section.

Here’s a quick code example of an escaping closure:

class TaskManager
{
    var onTaskFinished:(() -> Void)?

    func startLengthyTask(completionHandler: @escaping () -> Void)
    {
        // Store completion handler for later
        onTaskFinished = completionHandler

        // Do lengthy task
        // ...
    }

    func onLengthyTaskFinished() {
        onTaskFinished?() // Call completion handler
    }
}

let task = TaskManager()
task.startLengthyTask(completionHandler: {
    // Do this when task has finished...
})

What’s going on in the code? We’ve created a class called TaskManager, with a function called startLengthyTask(completionHandler:). At the end of the code, we’re calling this function and pass a closure into it.

The closure for the completionHandler parameter is passed into the function. Inside the function, the closure is assigned to the onTaskFinished property of the TaskManager class (of the same closure type). It’s temporarily stored there.

At some point in the future, the function onLengthyTaskFinished() is called by the TaskManager class. This will invoke the previously stored completion handler, which will execute the closure we passed into it.

First, note that the closure type for completionHandler is marked with that @escaping property. This is an escaping closure. But how does it escape? With this line:

onTaskFinished = completionHandler

The closure that’s passed into the function is now assigned to a property on the TaskManager class. The closure will continue to exist, until after the startLengthyTask() function returns.

Think of the closure as a number – you can use that number after the function exits, because it’s now assigned to that property. Beyond the startLengthyTask() function, you cannot use its completionHandler parameter.

But because its value – the closure – is stored in the onTaskFinished property, you can use it later on. The closure has escaped the function it was passed into!

What’s all this talk about “passing into”? That’s technobabble for using a variable as input for a function. The variable – or its value, really – is passed into the function as an argument. Want to learn more? Dive into functions, closures, value vs. reference types, and scope/context.

Why Use @Escaping?

What’s the problem with escaping closures? The code we’ve used so far runs OK. Why would we need to mark a closure as @escaping anyway?

In short, an escaping closure can cause a strong reference cycle if you use self inside the closure. Safety features like non-default, explicit @escaping are built into the Swift programming language, so you can remind yourself to check if you haven’t inadvertently caused a retain cycle.

The @escaping keyword isn’t a feature, it’s a warning sign! You’re letting the closure escape anyway, and once Swift finds out, it’ll help you avoid problems by forcing you to mark the closure as @escaping.

Before we move on, think back to the semantics of an “escaping” closure. In the previous example, you’ve seen that a closure can literally escape the function it was passed into by getting stored in a property outside that function.

When that escaping closure references self, or a strongly retained property, it will capture that reference strongly. If the escaping closure isn’t property released, you’ve created a strong reference cycle between self and the closure.

Here’s a quick shorthand:

  • A non-escaping closure can refer to self implicitly
  • An escaping closure must refer to self explicitly, or use a capture list

It’s impossible to create a strong reference cycle with a closure that’s non-escaping, because that closure doesn’t outlive the function it was passed into. The closure is released by the end of the function, and so are the values it captures.

It’s worth noting here that escaping closures work differently inside structs and classes. An escaping closure passed into a struct’s function cannot capture a mutable reference to self, because structs are value types. You cannot change a struct outside (“escaping”) a function marked with mutating; but only inside it.

It’s not that @escaping avoids a strong reference cycle; it just makes your code more explicit. It increases the safety of your code, just as with optionals, which decreases the likelyhood of accidentally creating bugs.

You can find an example of a strong reference cycle in a closure, and how to fix that, in this tutorial: How To Fix Strong Reference Cycles in Swift

Strong Reference Cycles in Escaping Closures

Let’s take a look at a comprehensive example. We’re going to create an escaping closure that’ll reference self and causes a strong reference cycle. We’ll also discuss 2 potential solutions.

First, some code:

class NetworkManager
{
    var onFinishRequest: ((String) -> Void)?

    func makeRequest(completionHandler: @escaping (String) -> Void)
    {
        onFinishRequest = completionHandler

        // Do lengthy task...

        self.finishRequest()
    }

    func finishRequest() {
        onFinishRequest?("some data")
    }
}

The NetworkManager class is responsible for managing a lengthy task, like an HTTP request. You can best compare it with a bare-bones URLSession, for example.

In the above code, you can see a function makeRequest(). You can pass a closure into this function, which is called when a lengthy task completes. The closure escapes the function, because it’s assigned to the onFinishRequest property. After the lengthy task completes, finishRequest() will invoke the closure and pass some string data with it.

Next up, a class that uses the above code:

class API
{
    var manager = NetworkManager()
    var data = ""

    init() {
        print("init API")
    }

    func getData()
    {
        manager.makeRequest(completionHandler: { data in

            self.data = data

            print("called completion handler with data: \(data)")
        })
    }

    deinit {
        print("deinit API")
    }
}

This API class has a function getData(), which calls the makeRequest() function on the NetworkManager object. It provides a closure for the completionHandler parameter of that function.

We’ve also marked the init and deinit functions. Especially the deinit function is important, because it’ll tell us if an instance of API is deallocated. When a strong reference cycle occurs, the API object won’t get deallocated.

What’s important to note here is that the closure strongly captures self. This happens with the self.data = data line of code. Why is that?

  1. In Swift, references are strong by default
  2. The completionHandler closure in makeRequest() is escaping
  3. Closures will hold onto reference values you use inside them; this is called capturing or “closing over” (which is where closures get their name!)

Differently said, for the closure to hold onto self, so it can assign the closure’s data to the data property of self, a strong reference needs to be created. Otherwise, self would be gone by the time the lengthy task finishes.

If you’re a bit unclear on ARC, strong references and closures, check out my 3-part series about them, starting with: Automatic Reference Counting (ARC) in Swift

Next up, we’re going to try out 3 scenarios:

  1. Run the above code and attempt to deallocate the API object
  2. Run the above code, and make sure self is referenced weakly
  3. Run the above code, and manually deallocate the closure

Let’s run the code, and see what happens. Here’s what we’re executing:

var api:API? = API()
api?.getData()
api = nil

// Output:
// init API
// called completion handler with data: some data

Would you look at that!? Even though we’re explicitly setting the api variable to nil, its deinit function is not called and the API object is not deallocated. This is a problem!

Let’s back up for a sec. What’s really going on here? Well, if we remove the call to getData(), you get the following output:

init API
deinit API

This tells us that setting a variable to nil will prompt the deallocation of its value. That’s how Automatic Reference Counting works; no one is holding onto the data, so it’s removed from memory to make space.

The culprit is getData(), but why? Inside that function, we’ve got a closure that captures a reference to self – the current class instance. A strong reference is created between self and the closure. This closure is retained strongly when it’s assigned to the onFinishRequest property.

Because the closure and self hold strongly onto each other, they cannot get deallocated. This is a strong reference cycle, which causes a memory leak. Less memory for other apps. That’s bad!

This reference cycle typically resolves itself if the closure doesn’t escape, but ours does! When a non-escaping closure is passed into a function, it’s automatically deallocated by the end of that function. This resolves the strong reference.

OK, how do you fix this? You’ve got 2 options:

  • Make the weak capture of self explicit with a capture list
  • Manually deallocate the closure when done, to break the cycle

First, the capture list. Check this out:

manager.makeRequest(completionHandler: { [weak self] data in     
    self?.data = data
    ···
})

The capture list [weak self] creates a weak reference between the closure and self. This prevents the strong reference cycle, because a weak reference doesn’t increase an object’s retain count. It does make the reference to self of an optional type, so you’ll need to unwrap self. (An alternative is unowned.)

When we run the code with the above change, here’s the output you get:

init API
called completion handler with data: some data
deinit API

Awesome. Crisis averted!

An alternative is to manually deallocate the closure. The trick is to break the cycle, and you can do that at both ends. Check this out:

class NetworkManager
{
    ···
    func finishRequest() {
        onFinishRequest?("some data")
        onFinishRequest = nil
    }
}

We’ve changed the finishRequest() function of the NetworkManager class. Right after invoking the closure that’s stored in the onFinishRequest property, we’re setting that property to nil. This deallocates that closure, which releases any strong references (if any) that the closure holds.

Resolving the strong reference cycle isn’t always viable this way, because it depends on the implementation of the NetworkManager code. It’ll need to hold onto that closure for some time, during which the strong reference persists.

Technically, making the onFinishRequest property weak would also break the strong reference cycle. It’ll also make the closure downright unusable, because the whole point is holding onto it until the lengthy task completes. You could also deallocate the NetworkManager instance, or overwrite the onFinishRequest property with something else. Of these alternatives, a capture list is the clearest and most reliable way to break the cycle.

Wrapping Up

In Swift, closures are non-escaping by default. This means that the closure can’t outlive the function it was passed into as a parameter. If you need to hold onto that closure after the function it was passed into returns, you’ll need to mark the closure with the keyword @escaping.

Escaping closures have an inherent risk: strong reference cycles. This happens when the closure strongly holds onto a reference, like self, and the closure itself is also retained. You can avoid a strong reference cycle by using a capture list, like [weak self].

Want to learn more? Check out these resources:

LearnAppMaking

LearnAppMaking

At LearnAppMaking.com, app developers learn how to build and launch awesome iOS apps.