Are Objective-C block parameters escaping in Swift, and what does NS_NOESCAPE change?
Predict first
An Objective-C method -each: has a block marked NS_NOESCAPE. Does this compile?
// Objective-C: - (void)each:(void (NS_NOESCAPE ^)(NSInteger))block;
func run(_ w: Walker, _ f: (Int) -> Void) {
w.each { f($0) }
}
- Yes
- No: escaping closure captures non-escaping parameter
Predict first
The method -later: has an ordinary block (it dispatches it later). Does the same shape compile?
// Objective-C: - (void)later:(void (^)(NSInteger))block; // dispatch_async inside
func run(_ w: Walker, _ f: (Int) -> Void) {
w.later { f($0) }
}
- Yes
- No: escaping closure captures non-escaping parameter
Mental model. A non-escaping block is a borrowed tool you must return before you leave the shop. An escaping one is a library book that the callee may keep at home.
The importer cannot know what the Objective-C method does with a block, so it assumes the safe-for-the-callee answer: an ordinary block parameter imports as an escaping closure. That has consequences for Swift callers, who must capture self explicitly in many cases, cannot capture a non-escaping closure parameter, and cannot use a captured inout. Marking the block NS_NOESCAPE (a promise that the callee only invokes it during the call) imports it as a non-escaping closure, which unlocks all of that and lets Swift skip the heap allocation for the context.
Add the attribute to block parameters of synchronous callbacks such as enumerate, sort and map-like methods. Do not add it for a handler that is stored or dispatched later; the compiler does not check the promise on the Objective-C side, so a lie leads to a call into a dead frame. Apple’s own enumeration APIs (for example enumerateObjects) are annotated, which is why their closures can capture local variables and inout parameters freely, while a completion handler is escaping.
The block is non-escaping, so it may use the non-escaping parameter f.
An unannotated block imports as escaping, and an escaping closure cannot capture a non-escaping parameter. The error appears when the function is compiled, not in a type-check-only pass.
Common traps
- Adding
NS_NOESCAPEto a block the method stores. Swift may have captured stack state that no longer exists. - Fighting “escaping closure captures non-escaping parameter” in Swift. The usual fix is an annotation in the header, not
withoutActuallyEscaping.
They might follow up with
- How is this the same as Swift’s own
@escapingrule? Non-escaping is the default in Swift; here it is the opposite default, because the importer cannot see the body. - What does the annotation cost? Nothing at runtime, it removes work.
Related questions
- 1.5.1 Escaping vs non-escaping closures?
- 4.7.2 How do Objective-C blocks differ from Swift closures, especially for capture?
- 4.5.3 How do you make an Objective-C API feel native in Swift?
← How do pointer and out-parameter conventions such as BOOL *stop… How do @optional protocol methods work, and how do you write a… →