Skip to main content

Swift Escaping Closures

When programming in Swift, you'll often work with closures as a way to pass functionality around in your code. One important concept to understand is the difference between escaping and non-escaping closures. This concept might seem advanced at first, but it's crucial for many common programming tasks like networking, asynchronous operations, and handling completion handlers.

Introduction to Escaping Closures

In Swift, closures are non-escaping by default. This means that a closure passed as a function argument must be executed within that function before the function returns. The closure "doesn't escape" the function's scope.

An escaping closure, on the other hand, is a closure that's called after the function it was passed to returns. The closure "escapes" the function's scope and can be stored or executed later.

Let's explore why this distinction matters and how to work with escaping closures.

Basic Syntax: The @escaping Attribute

To mark a closure parameter as escaping, we use the @escaping attribute. Here's the basic syntax:

swift
func someFunction(completion: @escaping () -> Void) {
// Function body
}

The @escaping attribute tells the Swift compiler that this closure will be used outside the scope of the function.

Non-Escaping vs. Escaping Closures: The Difference

Let's see the difference between non-escaping and escaping closures with examples:

Non-Escaping Closure (Default)

swift
func performOperation(with values: [Int], operation: (Int) -> Int) -> [Int] {
var results = [Int]()

for value in values {
results.append(operation(value))
}

return results
}

// Usage
let numbers = [1, 2, 3, 4, 5]
let multipliedNumbers = performOperation(with: numbers) { number in
return number * 2
}
print(multipliedNumbers) // Output: [2, 4, 6, 8, 10]

In this example, the operation closure is executed immediately within the function for each value. The closure doesn't need to outlive the function call.

Escaping Closure

swift
// Array to store closures for later execution
var storedClosures: [() -> Void] = []

func storeAndExecuteLater(closure: @escaping () -> Void) {
// Store the closure for later use
storedClosures.append(closure)
}

// Usage
storeAndExecuteLater {
print("This closure will be executed later!")
}

// Later in the code
storedClosures[0]() // Output: "This closure will be executed later!"

In this example, the closure is stored in an array and executed later, outside the storeAndExecuteLater function's scope.

Common Use Cases for Escaping Closures

1. Asynchronous Operations

The most common use case for escaping closures is with asynchronous operations, like network requests:

swift
func fetchData(completion: @escaping (Data?, Error?) -> Void) {
let url = URL(string: "https://api.example.com/data")!

URLSession.shared.dataTask(with: url) { data, response, error in
// This code executes after fetchData() has returned
completion(data, error)
}.resume()
}

// Usage
fetchData { data, error in
if let error = error {
print("Error: \(error)")
return
}

if let data = data {
print("Received data: \(data.count) bytes")
// Process the data
}
}

The completion closure escapes the fetchData function because it's called after the network request completes, which happens after fetchData() returns.

2. Completion Handlers

Completion handlers are a common pattern in Swift programming:

swift
func processImage(named: String, completion: @escaping (UIImage?) -> Void) {
// Simulate time-consuming image processing
DispatchQueue.global().async {
// Process the image asynchronously
let processedImage = UIImage(named: named)

// Return to main thread to update UI
DispatchQueue.main.async {
completion(processedImage)
}
}
}

// Usage
processImage(named: "photo") { image in
if let image = image {
imageView.image = image
}
}

3. Storage in Properties

When you need to store a closure in a property for later use:

swift
class TaskManager {
// Array to store tasks (as closures)
var tasks: [() -> Void] = []

func addTask(task: @escaping () -> Void) {
tasks.append(task)
}

func executeTasks() {
for task in tasks {
task()
}
tasks.removeAll()
}
}

let manager = TaskManager()
manager.addTask {
print("Task 1 completed")
}
manager.addTask {
print("Task 2 completed")
}

// Later
manager.executeTasks()
// Output:
// "Task 1 completed"
// "Task 2 completed"

Memory Management with Escaping Closures

When using escaping closures, you need to be careful about memory management to avoid retain cycles. A retain cycle occurs when two objects hold strong references to each other, preventing them from being deallocated.

Using self in Escaping Closures

When you use self inside an escaping closure, Swift requires you to reference it explicitly:

swift
class NetworkManager {
var userID: String = "user123"

func fetchUserData(completion: @escaping (UserData?) -> Void) {
// Using self requires explicit mention in escaping closures
let id = self.userID

NetworkService.shared.fetchUser(id: id) { userData in
// Using self here too
self.processUserData(userData)
completion(userData)
}
}

func processUserData(_ userData: UserData?) {
// Process the user data
}
}

Avoiding Retain Cycles with Weak References

To avoid retain cycles, use weak or unowned references to self:

swift
class ProfileViewController {
var username: String = "johndoe"

func loadProfileData() {
// Using weak self to avoid retain cycle
NetworkManager.shared.fetchUserProfile(username: username) { [weak self] profile in
// Need to check if self still exists
guard let self = self else { return }

self.updateUI(with: profile)
}

// Alternative using capture list with explicit type
NetworkManager.shared.fetchUserAvatar(username: username) { [weak self: ProfileViewController] profile in
// Need to check if self still exists
guard let self = self else { return }

self.updateAvatar(with: profile)
}
}

func updateUI(with profile: Profile) {
// Update UI
}

func updateAvatar(with avatar: UIImage) {
// Update avatar
}
}

Capturing and Modifying Variables

Escaping closures can capture and modify external variables, but there are some rules to be aware of:

swift
func counterFunction() -> () -> Int {
var counter = 0

// This is an escaping closure because it's being returned
let incrementCounter: () -> Int = {
counter += 1
return counter
}

return incrementCounter
}

let increment = counterFunction()
print(increment()) // Output: 1
print(increment()) // Output: 2
print(increment()) // Output: 3

In this example, the incrementCounter closure captures and modifies the counter variable, even after counterFunction has returned.

Real-World Example: Implementing a Request Queue

Let's create a practical example of using escaping closures to implement a simple network request queue:

swift
class RequestQueue {
typealias RequestCompletion = (Result<Data, Error>) -> Void

// Queue of pending requests
private var requests: [(URL, @escaping RequestCompletion)] = []
private var isProcessing = false

func enqueue(url: URL, completion: @escaping RequestCompletion) {
// Store the request and completion handler for later
requests.append((url, completion))
processNextIfNeeded()
}

private func processNextIfNeeded() {
// Don't start a new request if one is already in progress
guard !isProcessing, !requests.isEmpty else {
return
}

isProcessing = true
let (url, completion) = requests.removeFirst()

let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
// Handle the response
if let error = error {
completion(.failure(error))
} else if let data = data {
completion(.success(data))
}

// Mark as no longer processing and start the next request
self?.isProcessing = false
self?.processNextIfNeeded()
}

task.resume()
}
}

// Usage
let queue = RequestQueue()
let url1 = URL(string: "https://api.example.com/data1")!
let url2 = URL(string: "https://api.example.com/data2")!

queue.enqueue(url: url1) { result in
switch result {
case .success(let data):
print("Request 1 succeeded with \(data.count) bytes")
case .failure(let error):
print("Request 1 failed: \(error)")
}
}

queue.enqueue(url: url2) { result in
switch result {
case .success(let data):
print("Request 2 succeeded with \(data.count) bytes")
case .failure(let error):
print("Request 2 failed: \(error)")
}
}

This example demonstrates a practical use of escaping closures for managing asynchronous network requests in a queue.

Summary

Escaping closures are a powerful feature in Swift that allows you to:

  • Execute code asynchronously
  • Store closures for later execution
  • Implement completion handlers for asynchronous operations
  • Build flexible APIs that can defer execution

Key points to remember:

  1. Use @escaping when a closure needs to be called after the function returns
  2. Be careful with memory management to avoid retain cycles (use weak or unowned references)
  3. Explicitly reference self within escaping closures
  4. Escaping closures are commonly used for asynchronous operations, callbacks, and storing functions

Understanding escaping closures is essential for modern Swift development, especially when working with networking, concurrency, and asynchronous programming patterns.

Exercises

  1. Create a function that takes an array of strings and a completion closure that will be called after a 2-second delay with the sorted array.
  2. Implement a simple caching system that stores closures associated with string keys and can execute them later.
  3. Write a class that uses escaping closures to implement a retry mechanism for failed network requests.

Additional Resources

Understanding escaping closures is a stepping stone to mastering more advanced Swift concepts like structured concurrency with async/await, which builds upon these fundamental closure patterns.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)