What is @escaping in Swift closures

Swift Closure

Escaping Closures #

Escaping closures (@escaping) is a keyword used to indicate the life cycle of a closure that passes as an argument to the function. By prefix any closure argument with @escaping, you convey the message to the caller of a function that this closure can outlive (escape) the function call scope. Without escaping, a closure is non-escaping by default, and its lifecycle end along with function scope.

The following is an example of a non-escaping closure. A function that benchmark an execution time of a passing closure. A passing closure end when a function end. No closure escape from this function scope.

func benchmark(_ closure: () -> Void) {
let startTime = Date()
closure()
let endTime = Date()

let timeElapsed = endTime.timeIntervalSince(startTime)
print("Time elapsed: \(timeElapsed) s.")
}

How can a closure escape? #

The term @escaping might sound alienate to you, but an actual implementation to make a closure survive a calling function's scope is very simple. To make a passing closure survive when a function ended, you have to store it in a variable that's defined outside the function.

For example, I create a wrapper around CLLocationManager that exposes a new method to get a current location in the form of a callback. The getCurrentLocation function returns after it calls locationManager.requestLocation(), but the closure isn't called until we get back a user location from delegate callback. So, we store it in the completionHandler variable.

import Foundation
import CoreLocation

class MyLocationManager: NSObject, CLLocationManagerDelegate {
let locationManager: CLLocationManager

private var completionHandler: ((_ location: CLLocation) -> Void)? // <1>

override init() {
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
}

func getCurrentLocation(_ completion: @escaping (_ location: CLLocation) -> Void) { // <2>
completionHandler = completion // <3>
locationManager.requestLocation()
}

// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
completionHandler?(location) // <4>
completionHandler = nil // <5>
}
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {}
}

<1> A variable to store closure.
<3> We help a closure to survive a function scope by storing it outside the function scope.
<2> We need to put @escaping here to indicate our intention. Failing to do so would result in the following compile error.

Assigning non-escaping parameter 'completion' to an @escaping closure

<4> After we get location data back, we call the closure with that information.
<5> And then we release it from duty.

Nested escape #

Another way that closure can escape is by using that closure inside another escaping closure. In the following example, we pass a closure inside a dispatch queue.

func delay(_ closure: @escaping () -> Void) { // <1>
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 3) {
closure() // <2>
}
}

<1> You need to mark a closure as @escaping since <2> asyncAfter is a @escaping function.

public func asyncAfter(wallDeadline: DispatchWallTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)

You will get the following compile error message if you forget to escape your closure.

Escaping closure captures non-escaping parameter 'closure'

In this case, we don't need to know the underlying implementation of asyncAfter. All we need to know is DispatchQueue holds a reference to a passing closure and may outlive a call to DispatchQueue.main.asyncAfter. Anything passes into that closure also gets captured and retained by a dispatch queue.

Why we need to know whether it is @escaping? #

The fact that @escaping closure is stored (retain) somewhere else makes it possible to accidentally create a strong reference cycle. So, @escaping is like a precaution sign for a caller to stay alert when using them.

Let's take our previous MyLocationManager class as an example.

class DetailViewController: UIViewController {
let locationManager = MyLocationManager() // <1>

override func viewDidLoad() {
super.viewDidLoad()

locationManager.getCurrentLocation { (location) in
print("Get location: \(location)")
self.title = location.description // <2>
}
}
}

<1> DetailViewController own a locationManager.
<2> We reference a self (DetailViewController) in a passing closure, which is capture(retain) by a closure. And an escaping closure is own by MyLocationManager.

This results in a strong reference cycle.

The cycle will only break if we get a location update and set completionHandler to nil. If we failed to get a location, nobody would get a release, which leads to a memory leak.

// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
completionHandler?(location)
completionHandler = nil // <1>
}
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {} // <2>

<1> The strong reference cycle will break once we get a location.
<2> For fail case, the strong reference cycle remains (since we do nothing here).

Conclusion #

@escping is a way to inform those who consume our function that the closure parameter is stored somewhere and might outlive the function scope. If you see any @escaping keyword, you have to be cautious about what you passed into that closure since it may cause a strong reference cycle.


Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Tweet Share

If you enjoy this article, you can subscribe to the weekly newsletter.

Every Friday, you’ll get a quick recap of all articles and tips posted on this site — entirely for free.

← Home