How to use withoutActuallyEscaping


Greetings, traveler!

Closures in Swift are powerful tools that can be either escaping or non-escaping. By default, closures passed as function parameters are non-escaping, meaning they are not stored or used after the function completes.

However, some APIs require escaping closures, marked with @escaping, which can be stored or executed later. This article explores withoutActuallyEscaping, a utility that allows a non-escaping closure to be temporarily treated as escaping for compatibility with such APIs, while ensuring it is not actually stored or used beyond its intended scope.

The Problem with Non-Escaping Closures

Consider a scenario where you want to create a custom method similar to the standard library’s allSatisfy(_:) for collections. The goal is to check if all elements in a collection satisfy a given condition. Here’s an initial attempt:

extension Collection {    
    func allElementsSatisfy(_ predicate: (Element) -> Bool) -> Bool {
        self.lazy.filter { !predicate($0) }.isEmpty // ❌ Error
    }
}

This code uses lazy to create a lazy sequence, which evaluates elements only as needed, stopping after finding the first element that does not satisfy the predicate. However, this results in a compiler error:

Escaping closure captures non-escaping parameter ‘predicate’

The error occurs because lazy.filter expects an escaping closure, as the lazy sequence could potentially store the closure for later iteration. However, the predicate parameter is non-escaping, causing a type mismatch.

Solution

The withoutActuallyEscaping(_:do:) function provides a solution by allowing a non-escaping closure to be temporarily treated as escaping within a specific scope. Here’s how it can be applied:

extension Collection {
    func allElementsSatisfy(_ predicate: (Element) -> Bool) -> Bool {
        withoutActuallyEscaping(predicate) { escapablePredicate in
            self.lazy.filter { !escapablePredicate($0) }.isEmpty
        }
    }
}

In this code, withoutActuallyEscaping takes the non-escaping predicate closure and allows it to be passed to lazy.filter as if it were escaping. The closure is only valid within the scope of the do block, ensuring it is not stored or used beyond the function’s execution.

Example Usage

Here’s how the allElementsSatisfy method can be used:

struct User {
    var isAdmin: Bool
}

let users = [
    User(isAdmin: false),
    User(isAdmin: true),
    User(isAdmin: true)
]

let isAllAdmins = array.allElementsSatisfy { $0.isAdmin }

print(isAllAdmins) // false

This example checks if all users in the array are admins, returning false because one user is not an admin.

Another Practical Example

To further illustrate, consider a function that wraps a synchronous API requiring an escaping closure:

func synchronousAPI(_ closure: @escaping () -> Void) {
    closure()
}

func performSyncWork(_ nonEscapingClosure: () -> Void) {
    withoutActuallyEscaping(nonEscapingClosure) { escapable in
        synchronousAPI(escapable)
    }
}

Conclusion

This is a pretty rare but interesting case where we can use the Swift API in the way it is meant to be used.