Swift Tutorial

Swift Data Types

Swift Control Flow

Swift Functions

Swift Collections

Swift OOPs

Swift Additional Topics

Escaping and Non-Escaping Closures in Swift

In Swift, when we talk about closures, particularly in the context of function parameters, we often come across the terms "escaping" and "non-escaping." These terms describe the lifecycle of the closure relative to the function it's passed to.

1. Non-Escaping Closures (default):

  • When a closure is passed as an argument to a function and is called before the function returns, it's a non-escaping closure.
  • By default, closures are non-escaping in Swift.
  • Since the closure doesn't outlive the function call, there's no need to worry about potential retain cycles.

Example:

func performOperation(_ closure: () -> Void) {
    print("Start of function")
    closure()
    print("End of function")
}

performOperation {
    print("Closure is executed")
}

Output:

Start of function
Closure is executed
End of function

2. Escaping Closures:

  • When a closure is passed to a function and is stored to be called later, after the function returns, it's an escaping closure.
  • You declare an escaping closure by placing the @escaping attribute before the closure parameter��s type.
  • Since the closure can be called after the function returns, it might "escape" the current scope and be retained elsewhere (like in a property, by a global variable, etc.). As a result, you need to be mindful of retain cycles and often use capture lists like [weak self] or [unowned self].

Example:

var storedClosure: (() -> Void)?

func storeClosureForLater(_ closure: @escaping () -> Void) {
    storedClosure = closure
}

storeClosureForLater {
    print("Stored closure is now executed")
}

storedClosure?()

Output:

Stored closure is now executed

Why is this distinction important?

  1. Memory Management and Retain Cycles: Escaping closures can lead to retain cycles if you're not careful, especially when referencing self or other objects within the closure. Using capture lists ([weak self] or [unowned self]) can help prevent these retain cycles.

  2. Performance: Non-escaping closures allow the compiler to make certain optimizations, knowing that the closure's scope is limited to the lifecycle of the function.

  3. Immutable Variables: In non-escaping closures, you can freely mutate variables from the surrounding scope. In escaping closures, those variables need to be declared with var to be mutable.

In summary, the distinction between escaping and non-escaping closures is essential in Swift to manage memory efficiently, ensure performant code, and maintain a clear understanding of a closure's lifecycle and its interactions with other parts of the code.

  1. Swift escaping closures vs non-escaping closures:

    • Escaping closures are closures that outlive the function they are passed to, while non-escaping closures are executed within the scope of the function.
    • Example:
      func performOperation(completion: @escaping () -> Void) {
          DispatchQueue.main.async {
              completion()
          }
      }
      
      // Non-escaping closure
      func simpleOperation(completion: () -> Void) {
          // ...
          completion()
      }
      
  2. When to use escaping closures in Swift:

    • Escaping closures are used when a closure is passed as a parameter and can be called after the function returns, typically in asynchronous scenarios.
    • Example:
      func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
          // Asynchronous data fetching
      }
      
  3. How to declare escaping closures in Swift:

    • Declare escaping closures using @escaping in the parameter list.
    • Example:
      func performOperation(completion: @escaping () -> Void) {
          // ...
          completion()
      }
      
  4. Non-escaping closures in Swift examples:

    • Non-escaping closures are executed within the function and cannot outlive the function's scope.
    • Example:
      func simpleOperation(completion: () -> Void) {
          // ...
          completion()
      }
      
  5. Swift @escaping attribute explained:

    • The @escaping attribute is used to indicate that a closure parameter outlives the function it's passed to.
    • Example:
      func performOperation(completion: @escaping () -> Void) {
          // ...
          completion()
      }
      
  6. Escaping closures in asynchronous Swift functions:

    • Escaping closures are commonly used in asynchronous functions to handle callbacks that occur after the function returns.
    • Example:
      func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
          // Asynchronous data fetching
      }
      
  7. Benefits of non-escaping closures in Swift:

    • Non-escaping closures provide performance benefits and can be optimized by the compiler.
    • Example:
      func simpleOperation(completion: () -> Void) {
          // ...
          completion()
      }
      
  8. Handling retain cycles with escaping closures in Swift:

    • When using escaping closures, be cautious about retain cycles, especially when capturing self. Use weak or unowned references to avoid strong reference cycles.
    • Example:
      class MyClass {
          var closure: (() -> Void)?
          
          func setupClosure() {
              closure = { [weak self] in
                  self?.doSomething()
              }
          }
          
          func doSomething() {
              // Implementation
          }
      }
      
  9. Autoclosures and escaping in Swift:

    • Autoclosures can be marked as escaping if they are passed as escaping closures to other functions.
    • Example:
      func performOperation(completion: @escaping @autoclosure () -> Void) {
          DispatchQueue.main.async {
              completion()
          }
      }
      
  10. Escaping closures in completion handlers:

    • Completion handlers, commonly used in asynchronous operations, often require escaping closures to handle callbacks.
    • Example:
      func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
          // Asynchronous data fetching
      }
      
  11. Closure capturing list in Swift:

    • The capturing list in a closure is used to define how variables are captured. It's especially important when dealing with escaping closures to avoid retain cycles.
    • Example:
      class MyClass {
          var closure: (() -> Void)?
          
          func setupClosure() {
              closure = { [weak self, unowned someObject] in
                  guard let strongSelf = self else { return }
                  strongSelf.doSomething()
              }
          }
          
          func doSomething() {
              // Implementation
          }
      }
      
  12. Swift escaping closures in APIs:

    • APIs often use escaping closures for asynchronous operations. It's important to document whether a closure parameter is escaping.
    • Example:
      func performOperation(completion: @escaping () -> Void) {
          // ...
          completion()
      }