Essential iOS Interview Questions on Escaping and Non-Escaping Closures

NitinNitin
6 min read

Escaping and non-escaping closures demonstrate strong functions and closure skills and problem-solving abilities in your interviews, also enabling you to write efficient and concise code. Be ready to prepare these questions for your iOS interviews as these are the most common questions for interviews.

Escape: to manage to get away from a place where you do not want to be;

Q. What is the difference between escaping and non-escaping closures?

Swift closures can capture and store references to constants and variables from the context within which they are defined. These captured values can lead to a reference cycle. This is where the closure captures a reference to a value that also has a strong reference back to the closure, causing a memory leak.

To avoid memory leaks, Swift provides two types of closure: escaping and non-escaping closures.

Non-Escaping Closures

  • A non-escaping closure guarantees to be executed before the function it is passed to returns.

  • The compiler knows that the closure won’t be used outside the function and optimise the code accordingly.

  • Non-escaping closures are the default type of closure in Swift.

// Non-Escaping Closure
func execute(closure: () -> Void) {
    print("Executing non-escaping closure")
    closure()
    print("Finished executing non-escaping closure")
}

execute {
    print("This is a non-escaping closure")
}

// Output:
// Executing non-escaping closure
// This is a non-escaping closure
// Finished executing non-escaping closure

Escaping Closures

  • An escaping closure is passed as an argument to a function but called after the function returns.

  • The closure is stored in memory until it is called, which means it can outlive the function that created it. In other words, the closure “escapes” from the function.

// Escaping Closure
var escapingClosureArray: [() -> Void] = []

func addEscapingClosureToQueue(closure: @escaping () -> Void) {
    print("Adding escaping closure to queue")
    escapingClosureArray.append(closure)
}

addEscapingClosureToQueue {
    print("This is an escaping closure")
}

print("Before closure execution")
escapingClosureArray.forEach { $0() }
print("After closure execution")

// Output:
// Adding escaping closure to queue
// Before closure execution
// This is an escaping closure
// After closure execution

Q: When should you use the Escaping closure?

A escaping closure is useful in most of the cases when you are performing an asynchronous task. These are some most common use cases:

Network requests: When making network requests, it’s common to pass an escaping closure to the function that performs the request. The closure is executed when a response is received from the server. This allows you to update the user interface or perform other tasks based on the response.

Asynchronous APIs: Many APIs in iOS and macOS are asynchronous, meaning they don’t block the main thread while executing. When using these APIs, you often need to pass an escaping closure to handle the response or completion of the operation.

Custom animations: If you’re creating custom animations in your app, you may want to pass an escaping closure to a function that performs the animation. The closure is executed when the animation is complete, allowing you to perform additional tasks or update the user interface.

Delegation: Sometimes, you may want escaping closures as an alternative to delegation. Instead of creating a separate delegate object, you can pass a closure that handles the event to the function that triggers the event.

Q: Why do you need escaping closures in Swift? Provide an example of a practical use case.

Let’s consider an example of a function that fetches data from a URL using an escaping closure:

// Asynchronous operation using escaping closure
func fetchData(from url: URL, completion: @escaping (Data?) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            print("Error: \(error)")
            completion(nil)
            return
        }
        completion(data)
    }.resume()
}

let url = URL(string: "https://picsum.photos/200/300")!
fetchData(from: url) { data in
    if let data = data {
        print("Data received: \(data)")
        // Process the fetched data further...
    } else {
        print("Failed to fetch data.")
    }
}

In this example, the fetchData function takes a URL and an escaping closure called completion. The completion closure is used to handle the fetched data once the network request is complete.

The URLSession.dataTask function is an asynchronous operation, and it requires a closure to handle the response or error. Since this closure is escaping (marked with @escaping), it will be captured and stored for later execution, after the fetchData function returns.

What if you call the fetchData() without escaping closure?

func fetchData(from url: URL, completion: (Data?) -> Void) {

}

// Error
error: escaping closure captures non-escaping parameter 'completion'
    URLSession.shared.dataTask(with: url) { data, _, error in

Without escaping closures, you would not be able to handle asynchronous operations like network requests effectively, as the closure passed to fetchData would not persist beyond the function call.

Q: How can you prevent retain cycles when using escaping closures?

Retain cycles occur when a closure captures a reference to a class instance, and the class instance, in turn, retains a strong reference to the closure.

To prevent retain cycles when using escaping closures, you can use either a capture list with a weak reference ([weak self]) or an unowned reference ([unowned self]) inside the closure. Both methods ensure that the closure does not create a strong reference cycle with the captured instance.

When you use a weak reference in the closure capture list ([weak self]), the reference to the captured instance will be automatically set to nil if the instance is deallocated. This means you need to handle the possibility that the weak reference might be nil when accessed inside the closure.

Suppose you want to create a Timer wrapper class that allows you to schedule a repeating timer while avoiding retain cycles. We’ll use escaping closures to handle the timer’s callback. To prevent a retain cycle, we’ll use a weak reference in the closure capture list.

class TimerWrapper {

    private var timer: Timer?

    func startTimer(interval: TimeInterval, completion: @escaping () -> Void) {

        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in

            // Check if the TimerWrapper instance still exists before executing the closure
            guard let self = self else {
                print("self (TimerWrapper) does not exists.")
                return
            }
            completion()
        }
    }

    func stopTimer() {
        timer?.invalidate()
        timer = nil
    }
}

class RepeatedTask {

    var timerWrapper: TimerWrapper?

    func startRepeatingTask() {
        timerWrapper = TimerWrapper()
        timerWrapper?.startTimer(interval: 5.0) { [weak self] in

            // This closure captures 'self' weakly to avoid retain cycle
            guard let self = self else {
                print("self (RepeatedTask) does not exists.")
                return
            }

            print("Timer fired. Performing the repeating task.")
            // Perform the repeating task...
        }
    }

    func removeTimeWrapper() {
        timerWrapper?.stopTimer()
        timerWrapper = nil
    }
}

var task: RepeatedTask? = RepeatedTask()
task?.startRepeatingTask()

DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: {
    print("deallocating task object...")
    task = nil
})


// Output:
// deallocating task object...
// self (TimerWrapper) does not exists.

The completion closure in the startTimer method of TimerWrapper captures self weakly using [weak self] in the capture list, avoiding a retain cycle. When the timer fires and the closure is executed, it first checks whether self still exists (i.e. the RepeatedTask instance still exists) before performing the repeating task.


Start your preparation for iOS interviews with “iOS Interview Handbook“, includes:
✅ 290+ top interview Q&A covering a wide range of topics
✅ 520+ pages with examples in most of the questions
✅ Expert guidance and a roadmap for interview preparation
✅ Exclusive access to a personalised 1:1 session for doubts

Grab your copy now


Thank you for taking the time to read this article! If you found it helpful, don’t forget to like, share, and leave a comment with your thoughts or feedback. Your support means a lot!

Keep reading,
— Swiftable

0
Subscribe to my newsletter

Read articles from Nitin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nitin
Nitin

Lead Software Engineer, Technical Writer, Book Author, A Mentor