Non-escaping optional closures in UIKit?

From my understanding, optional closures are always implicitly escaping because they are boxed in an Optional that could theoretically escape.

I am currently writing a function that takes a (non-optional) closure and forwards it to UITableView's performBatchUpdates(_:completion:). I was fully expecting to have to annotate the parameter as @escaping, but it compiles just fine without the annotation. If I write a method with the same signature as performBatchUpdates(_:completion:) and try to pass the closure to this method, however, the compiler correctly complains:

extension UITableView {
    func f(updateData: () -> Void) {

        // these two work
        self.performBatchUpdates(updateData)
        self.performBatchUpdates({ updateData() })
        
        // these two don't
        self._performBatchUpdates(updateData) // Cannot convert value of type '() -> Void' to expected argument type '(() -> Void)?'
        self._performBatchUpdates({ updateData() }) // Escaping closure captures non-escaping parameter 'updateData'
    }

    // Same signature as UIKit's _performBatchUpdates(_:completion:)
    // https://developer.apple.com/documentation/uikit/uitableview/2887515-performbatchupdates
    func _performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) {
    }
}

What kind of magic is this? Are there some hidden annotations in UIKit's interface to allow for non-escaping optional closures?

1 Like

I think the first closure is annotated with NS_NOESCAPE (i.e. closure type is void (NS_NOESCAPE ^ _Nullable)(void))) hence no error.

1 Like

Thanks for the reply, that's indeed the case.

There's no way to replicate that kind of interface in pure Swift, is there?

1 Like

I don't think so, but I think you can use withoutActuallyEscaping to pass the closure:

withoutActuallyEscaping(updateData) { self._performBatchUpdates($0) }
3 Likes