What is @escaping in Swift closures
Table of Contents
What is @escaping in Swift
Escaping closures (@escaping
) is a keyword that provides information about the life cycle of a closure that passes as an argument to the function.
By prefixing 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.
You can easily support sarunw.com by checking out this sponsor.
Debug 10x faster with Proxyman: Your ultimate tool to capture HTTPs requests/ responses, natively built for your iPhone and macOS. Special deal for Black Friday: Get 30% off for all Proxyman licenses with code “BLACKFRIDAY2024”.
Non-escaping closure
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 benchmarks an execution time of a passing closure.
- A passing closure end when a function end.
- No closure escaped 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.")
}
Escaping closure
The term @escaping
might sound alienating to you, but an actual implementation to make a closure survive a calling function's scope is very simple.
We can either.
Store a closure as a variable
To make a passing closure outlive the scope of the function, you have to store it in a variable outside the scope of 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 end after it callslocationManager.requestLocation()
. - But we get the result later in the delegate callback.
- So, we need to store completion closure in the instance variable,
completionHandler
.
import Foundation
import CoreLocation
class MyLocationManager: NSObject, CLLocationManagerDelegate {
let locationManager: CLLocationManager
// 1
private var completionHandler: ((_ location: CLLocation) -> Void)?
override init() {
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
}
// 2
func getCurrentLocation(_ completion: @escaping (_ location: CLLocation) -> Void) {
// 3
completionHandler = completion
locationManager.requestLocation()
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
// 4
completionHandler?(location)
// 5
completionHandler = nil
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {}
}
1 A variable to store closure.
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
3 We store a completion closure in a class variable. This makes it survive the function scope.
4 After we get the location data back, we call the closure with that information.
5 And then we release it from duty.
Since CLLocationManager
uses a delegate pattern, we will get the current location later in another delegate method, locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
. So, we need to mark a closure escaped in this case.
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.
// 1
func delay(_ closure: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 3) {
// 2
closure()
}
}
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)
If you forget to escape your closure, you will get the following compile error message.
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 that passes into that closure also gets captured and retained by a dispatch queue.
Why do we need to know whether it is @escaping?
The fact that @escaping
closure is stored (retained) 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 {
// 1
let locationManager = MyLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.getCurrentLocation { (location) in
print("Get location: \(location)")
// 2
self.title = location.description
}
}
}
1 DetailViewController
own a locationManager
.
2 We reference a self
(DetailViewController
) in a passing closure, which is captured (retained) by a closure. And an escaping closure is owned 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, leading to a memory leak.
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
completionHandler?(location)
// 1
completionHandler = nil
}
}
// 2
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {}
1 The strong reference cycle will break once we get a location.
2 For the fail case, the strong reference cycle remains (since we do nothing here).
You can easily support sarunw.com by checking out this sponsor.
Debug 10x faster with Proxyman: Your ultimate tool to capture HTTPs requests/ responses, natively built for your iPhone and macOS. Special deal for Black Friday: Get 30% off for all Proxyman licenses with code “BLACKFRIDAY2024”.
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.
Read more article about Swift, Closure, or see all available topic
Enjoy the read?
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. No strings attached. Unsubscribe anytime.
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 Buy me a coffee Tweet ShareBetter print debugging with Xcode breakpoints
Print debugging is the simplest form of debugging technique, but it possesses some drawbacks. Let's see how we can make it better by using Xcode breakpoints.
Understanding Date and DateComponents
Date and time might be among your list of the hardest things in programming (It is for me). Today, I'm going to talk about a basic concept of a Date and its companion DateComponents.