Closures in Swift:
A closure is a block of code that can be executed later.
Closures in Swift can capture and store references to variables and constants from the context in which they are defined. This is known as closing over those variables.
The closure can then refer to and modify the values of those captured variables, even if the scope that defined the variables no longer exists.
Closure Expression Syntax
The most general form of a closure expression syntax is:
{ (parameters) -> returnType in statements }
For example, a closure that adds two integers:
let addClosure = { (a: Int, b: Int) -> Int in return a + b }
This closure can be called like a function:
let sum = addClosure(3, 5) print(sum) // Output: 8
Simplifying Closure Syntax
Swift allows you to simplify closure syntax using type inference, shorthand argument names, and trailing closure syntax.
Type Inference
Swift can infer the types of parameters and the return type of the closure based on context, allowing you to omit them.
let addClosure: (Int, Int) -> Int = { (a, b) in return a + b }
Shorthand Argument Names
Swift provides automatic argument names for inline closures, allowing you to use $0
, $1
, $2
, etc., to refer to the closure’s arguments.
let addClosure: (Int, Int) -> Int = { return $0 + $1 }
Implicit Return
For single-expression closures, Swift allows you to omit the return
keyword.
let addClosure: (Int, Int) -> Int = { $0 + $1 }
Trailing Closure Syntax
If a closure is the last argument in a function, you can write it as a trailing closure outside the parentheses.
func performOperation(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int { return operation(a, b) } let result = performOperation(a: 2, b: 3) { $0 + $1 } print(result) // Output: 5
Capturing Values
Closures can capture and store references to variables and constants from their surrounding context. This allows them to work with those values even after the original scope has ended.
func makeIncrementer(incrementAmount: Int) -> () -> Int { var total = 0 let incrementer: () -> Int = { total += incrementAmount return total } return incrementer } let incrementByTwo = makeIncrementer(incrementAmount: 2) print(incrementByTwo()) // Output: 2 print(incrementByTwo()) // Output: 4
In this example, the incrementer
closure captures the total
variable and incrementAmount
constant from its surrounding context.
Autoclosures
An autoclosure is a closure that’s automatically created to wrap an expression that’s being passed as an argument to a function.
It doesn’t take any arguments and when it’s called, it returns the value of the expression that’s wrapped inside it.
func serve(customer customerProvider: @autoclosure () -> String) { print("Now serving \(customerProvider())!") } serve(customer: "Sid") // Output: Now serving Sid!
This makes the code more readable and clean.
An example with autoclosure and without autoclosure:
Let’s explore an example with and without using @autoclosure
to understand its benefits and differences.
Scenario: Logging Messages Based on Debug Flag:
Suppose we have a function to log messages only if a debug flag is set. We want to make sure the logging message is constructed only if necessary to avoid unnecessary performance costs.
Without @autoclosure:
Without using @autoclosure
, you need to pass a closure that returns the message:
var isDebugMode = true func log(messageProvider: () -> String) { if isDebugMode { print(messageProvider()) } } // Usage log(messageProvider: { "This is a debug message" })
In this example:
- The
log
function takes a closuremessageProvider
that returns aString
. - The message is generated only if
isDebugMode
istrue
, avoiding the cost of creating the message when not in debug mode. - The usage of the
log
function requires explicitly wrapping the message in a closure.
With @autoclosure:
Using @autoclosure
, you can simplify the function call by automatically wrapping the provided expression in a closure:
var isDebugMode = true func log(message: @autoclosure () -> String) { if isDebugMode { print(message()) } } // Usage log(message: "This is a debug message")
In this example:
- The
log
function uses the@autoclosure
attribute for themessage
parameter, which automatically wraps the provided expression in a closure. - The message is still generated only if
isDebugMode
istrue
. - The usage of the
log
function is simpler and more readable because you don’t need to explicitly wrap the message in a closure.
Detailed Explanation: –
Without @autoclosure
- Function Definition:
func log(messageProvider: () -> String) { if isDebugMode { print(messageProvider()) } }
- The
log
function takes a closuremessageProvider
that returns aString
. - The closure is called inside the function only if
isDebugMode
istrue
.
2. Function Call:
log(messageProvider: { "This is a debug message" })
- The message is wrapped in a closure and passed to the
log
function. - This ensures the message is created only when needed.
With @autoclosure
- Function Definition:
func log(message: @autoclosure () -> String) { if isDebugMode { print(message()) } }
- The
log
function takes a parametermessage
with the@autoclosure
attribute. - The provided expression is automatically wrapped in a closure.
2. Function Call:
log(message: "This is a debug message")
- The message is provided as a simple string expression.
- The
@autoclosure
attribute ensures the expression is not evaluated until it is called within the function.
Benefits of @autoclosure
- Simplified Syntax: The function call is more concise and readable without the need for explicit closure syntax.
- Deferred Evaluation: The expression is still evaluated lazily, only when needed, preserving the performance benefits.
Conclusion
Using @autoclosure
can make your code cleaner and more expressive by automatically converting an expression into a closure. This is particularly useful for logging, assertions, and other scenarios where you want to defer the evaluation of an expression until it’s actually needed.
Escaping Closures
An escaping closure is a closure that’s passed as an argument to a function but is called after the function returns.
This means the closure escapes the scope of the function.
To indicate that a closure is allowed to escape, you write @escaping
before the parameter’s type.
Example :
Let’s consider an example where we use an escaping closure to defer execution:
var completionHandlers: [() -> Void] = [] func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) } someFunctionWithEscapingClosure { print("This closure is escaping") } completionHandlers[0]() // Output: This closure is escaping
Step-by-Step Explanation
Step 1: Declaration of completionHandlers
Array
var completionHandlers: [() -> Void] = []
- We declare an array
completionHandlers
that will store closures of type() -> Void
. - This array can hold any number of closures that do not take any parameters and do not return any values.
Step 2: Definition of someFunctionWithEscapingClosure
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) }
- The function
someFunctionWithEscapingClosure
takes a closurecompletionHandler
as a parameter. - The
@escaping
attribute indicates that the closure can outlive the function call. - Inside the function, the closure is appended to the
completionHandlers
array.
Step 3: Calling someFunctionWithEscapingClosure
someFunctionWithEscapingClosure { print("This closure is escaping") }
- When
someFunctionWithEscapingClosure
is called, the provided closure (print("This closure is escaping")
) is passed as thecompletionHandler
parameter. - The function appends this closure to the
completionHandlers
array.
Step 4: Storing the Closure
- The closure is stored in the
completionHandlers
array. - At this point,
someFunctionWithEscapingClosure
has returned, but the closure remains in memory because it’s stored in the array.
Step 5: Executing the Closure
completionHandlers[0]() // Output: This closure is escaping
- We call the first closure in the
completionHandlers
array. - The closure executes and prints “This closure is escaping”.
Under-the-Hood Working
- Closure Capture: When the closure is passed to
someFunctionWithEscapingClosure
, it captures references to any constants and variables from its surrounding context (if any). In this case, there are no captured variables. - Escaping Attribute: The
@escaping
attribute tells Swift that the closure might be stored and used after the function it was passed to returns. This changes how Swift manages the memory for the closure. - Memory Management:
- Without @escaping: By default, closures are non-escaping. Swift optimizes memory management for non-escaping closures by allocating memory on the stack, making them faster but limited to the scope of the function.
- With @escaping: When a closure is marked as
@escaping
, Swift allocates memory for the closure on the heap. This allows the closure to exist beyond the scope of the function call.
- Storing the Closure: When
completionHandler
is appended to thecompletionHandlers
array, the reference count for the closure is incremented. The array now holds a strong reference to the closure, preventing it from being deallocated when the function returns. - Executing the Closure: When the closure is called from the
completionHandlers
array, it executes its stored code. If the closure had captured any variables from its original context, it would be able to access and modify them at this point.
Conclusion :
Escaping closures allow you to defer work and keep closures around for later use, even after the function that took them as parameters has returned. Understanding how Swift handles escaping closures under the hood helps you write efficient and safe code, particularly in managing memory and avoiding retain cycles.
Non-Escaping Closures
Non-escaping closures are the default type of closures in Swift.
They are guaranteed to be executed before the function they are passed to returns. This means they are used within the function’s scope and do not escape it.
Because of this, Swift can perform certain optimizations with non-escaping closures.
Example:
Let’s consider a simple example where we use a non-escaping closure:
func someFunctionWithNonEscapingClosure(closure: () -> Void) { closure() } someFunctionWithNonEscapingClosure { print("This is a non-escaping closure") }
Step-by-Step Explanation:
Step 1: Definition of someFunctionWithNonEscapingClosure
func someFunctionWithNonEscapingClosure(closure: () -> Void) { closure() }
- The function
someFunctionWithNonEscapingClosure
takes a closureclosure
as a parameter. - The closure is of type
() -> Void
, meaning it takes no parameters and returns nothing. - The function immediately calls
closure()
within its body.
Step 2: Calling someFunctionWithNonEscapingClosure
someFunctionWithNonEscapingClosure { print("This is a non-escaping closure") }
- When
someFunctionWithNonEscapingClosure
is called, the provided closure (print("This is a non-escaping closure")
) is passed as theclosure
parameter. - The function immediately executes this closure within its body.
Under-the-Hood Working
- Closure Capture: When the closure is passed to
someFunctionWithNonEscapingClosure
, it captures references to any constants and variables from its surrounding context (if any). In this simple example, there are no captured variables. - Non-Escaping by Default: By default, closures are non-escaping in Swift. This allows Swift to perform optimizations, as it knows the closure will not be stored or executed after the function returns.
- Memory Management:
- Stack Allocation: Swift can allocate memory for non-escaping closures on the stack. Stack allocation is fast and efficient but limited to the scope of the function call.
- No Retain Cycle Issues: Since non-escaping closures are guaranteed to be executed within the function’s scope, there’s no risk of retain cycles related to the closure itself.
- Executing the Closure: The closure is executed immediately within the function body. Once the closure completes, and the function returns, the memory used by the closure is deallocated if it was allocated on the stack.
Capturing Values in Non-Escaping Closures
Even though non-escaping closures are limited to the scope of the function, they can still capture and modify variables from their surrounding context.
func someFunctionWithNonEscapingClosure(closure: () -> Void) { closure() } var x = 10 someFunctionWithNonEscapingClosure { x = 20 } print(x) // Output: 20
In this example, the closure captures the variable x
and modifies it within the function scope.
Conclusion
Non-escaping closures in Swift are executed within the scope of the function they are passed to, allowing Swift to optimize their memory management. This makes them fast and efficient. Understanding the default behavior and optimizations of non-escaping closures helps in writing performant and safe Swift code.
Non-escaping closures are the default and are often simpler to reason about since they do not have the complexities associated with escaping closures, such as managing retain cycles and heap allocations.
Real-World Scenarios for Using Escaping and Non-Escaping Closures:
Non-Escaping Closures
Scenario: Sorting Arrays
Non-escaping closures are commonly used in scenarios where the closure is executed immediately within the scope of a function call. A typical example is sorting an array using a custom comparison closure.
let names = ["John", "Jane", "Alex", "Chris"] let sortedNames = names.sorted { (first, second) -> Bool in return first < second } print(sortedNames) // Output: ["Alex", "Chris", "Jane", "John"]
In this example:
- The
sorted(by:)
method takes a non-escaping closure as its argument. - The closure is executed immediately to compare elements in the array, and it does not escape the
sorted(by:)
method.
Escaping Closures
Scenario: Network Requests
Escaping closures are necessary when the closure needs to be called after the function it was passed to has returned. A common scenario is asynchronous network requests where the response may arrive at a later time.
import Foundation func fetchData(from url: String, completion: @escaping (Data?, Error?) -> Void) { guard let url = URL(string: url) else { completion(nil, NSError(domain: "Invalid URL", code: 400, userInfo: nil)) return } URLSession.shared.dataTask(with: url) { data, response, error in completion(data, error) }.resume() } fetchData(from: "https://jsonplaceholder.typicode.com/posts/1") { data, error in if let error = error { print("Error: \(error)") } else if let data = data { print("Data: \(data)") } }
In this example:
- The
fetchData(from:completion:)
function takes an escaping closurecompletion
because it needs to be called after the network request completes. - The closure captures the network response data or error and is called asynchronously inside the URLSession data task.
Detailed Explanation
Non-Escaping Closure:
In the sorting example, the closure provided to sorted(by:)
is a non-escaping closure because:
- It is used immediately within the function to compare array elements.
- Swift can optimize its memory management by allocating the closure on the stack since it doesn’t need to persist after the function returns.
Benefits of Non-Escaping Closures:
- Improved performance due to stack allocation.
- Simplified memory management without the risk of retain cycles.
Escaping Closure:
In the network request example, the closure provided to fetchData(from:completion:)
is an escaping closure because:
- The network request is asynchronous, and the closure must be retained and called after the function returns.
- The closure is stored and executed later when the network request completes, requiring it to “escape” the scope of the function.
Benefits of Escaping Closures:
- Enable asynchronous operations where the closure needs to be executed after a delay or on a different thread.
- Allow deferred execution of code, which is essential for tasks like network requests, file I/O, and animations.
Example of Retain Cycle in Closure
Consider a class ViewController
that has a method using a closure:
class ViewController { var title: String var callback: (() -> Void)? init(title: String) { self.title = title } func setupCallback() { callback = { print("Title is \(self.title)") } } deinit { print("\(title) is being deinitialized") } } var vc: ViewController? = ViewController(title: "Home") vc?.setupCallback() vc = nil // ViewController is not deallocated
Explanation
- The
ViewController
class has atitle
property and acallback
property, which is a closure. - The
setupCallback
method assigns a closure to thecallback
property, which capturesself
. - The
deinit
method is used to observe when theViewController
instance is deallocated. - An instance of
ViewController
is created and assigned to thevc
variable. - The
setupCallback
method is called, setting up the closure that capturesself
. - The
vc
variable is set tonil
, but theViewController
instance is not deallocated because of the retain cycle.
Retain Cycle:
In the example above, a retain cycle is created as follows:
- The
ViewController
instance has a strong reference to the closure through thecallback
property. - The closure captures and holds a strong reference to
self
(theViewController
instance).
As a result, the ViewController
instance and the closure keep each other alive, preventing deallocation.
Breaking the Retain Cycle
To break the retain cycle, you can use a weak or unowned reference within the closure to capture self
.
Using weak
Reference
A weak reference does not keep a strong hold on the object it refers to, allowing the object to be deallocated if there are no other strong references.
class ViewController { var title: String var callback: (() -> Void)? init(title: String) { self.title = title } func setupCallback() { callback = { [weak self] in guard let self = self else { return } print("Title is \(self.title)") } } deinit { print("\(title) is being deinitialized") } } var vc: ViewController? = ViewController(title: "Home") vc?.setupCallback() vc = nil// "Home is being deinitialized" is printed
In this updated example:
- The closure captures
self
weakly using[weak self]
. - Inside the closure,
self
is safely unwrapped usingguard let self = self else { return }
. - When
vc
is set tonil
, theViewController
instance is properly deallocated, breaking the retain cycle.
Using unowned
Reference
An unowned reference also does not keep a strong hold on the object it refers to, but it assumes the object will never be nil
once it is set. This should be used when you are sure the object will not be deallocated while the reference is used.
class ViewController { var title: String var callback: (() -> Void)? init(title: String) { self.title = title } func setupCallback() { callback = { [unowned self] in print("Title is \(self.title)") } } deinit { print("\(title) is being deinitialized") } } var vc: ViewController? = ViewController(title: "Home") vc?.setupCallback() vc = nil// "Home is being deinitialized" is printed
In this example:
- The closure captures
self
unowned using[unowned self]
. - When
vc
is set tonil
, theViewController
instance is properly deallocated, and there is no retain cycle.
Conclusion
Retain cycles in closures can prevent objects from being deallocated, leading to memory leaks. By using weak
or unowned
references, you can break retain cycles and ensure proper memory management in your Swift applications.
Frequently Asked Interview Questions on Closures and Capturing Closures with Code Examples:
Q. How you can define a simple closure?
Ans: A simple closure that takes no parameters and returns no value can be defined as follows:
let simpleClosure = { print("This is a simple closure") } // Call the closure simpleClosure()// Output: This is a simple closure
In this example:
simpleClosure
is a constant that holds a closure.- The closure contains a single print statement.
- The closure is called using
simpleClosure()
.
Q. How you can define a closure with parameters ?
Ans: A closure can also take parameters and return a value. Here’s an example of a closure that takes two integers and returns their sum:
let addClosure: (Int, Int) -> Int = { (a, b) in return a + b } // Call the closure let result = addClosure(3, 5) print(result)// Output: 8
In this example:
addClosure
is a constant that holds a closure which takes two integers (a
andb
) and returns their sum.- The closure is called using
addClosure(3, 5)
, and the result is printed.
Q. Define a closure as a function parameter?
Ans. In Swift, you can define a closure as a function parameter to allow the function to accept blocks of code that it can execute during its execution. This is useful for things like completion handlers, callbacks, or any situation where you need to pass some behavior into a function.
Let’s create a simple example where a function takes a closure as a parameter and calls that closure.
func performOperation(operation: () -> Void) { print("Starting operation...") operation() print("Operation finished.") } performOperation { print("Performing the operation") } Starting operation... Performing the operation Operation finished.
Step-by-Step Explanation:
- The
performOperation
function takes a closureoperation
of type() -> Void
as a parameter. - The closure
operation
takes no parameters and returns nothing. - Inside the function, the closure is called between two print statements.
- The
performOperation
function is called with a closure that prints “Performing the operation”. - The closure is executed between the print statements in the function.
Q. What will be output of below code snippet:
var name= "Sid" let closure = { print("My name is: \(name) ") } closure()
Ans:
My name is: Sid
explanation:
In Swift, closures capture and store references to any constants and variables from the surrounding context in which they are defined. This is why the closure can access the variable name
even though it was declared outside the closure. The value of name
at the time the closure is executed is what gets printed.
Q. What will be output of below code snippet:
var name = "Sid" let closure = { [name] print("My name is \(name) ") } name = "Shivi" closure()
Ans:
My name is Shivi
Q. What will be output of below code snippet:
var name = "Sid" let closure = { [name] in print("My name is \(name) ") } name = "Shivi" closure()
Ans:
My name is Sid
Q. What will be output of below code snippet:
var name = "Sid" var technology = "iOS" let closure = { [name] in print("My name is \(name) ") print("I am an \(technology) developer") } name = "Shivi" technology = "Android" closure()
Ans.
My name is Sid
I am an Android developer
Q. What will be output of below code snippet:
var name = "Sid" var technology = "iOS" let closure = { [name, technology] in print("My name is \(name) ") print("I am an \(technology) developer") } name = "Shivi" technology = "Android" closure()
Ans.
My name is Sid I am an iOS developer
Q. What will be output of below code snippet:
func execute() -> (Int) -> Int { var input = 0 return { output in input += output return input } } let exe = execute() let a = exe(5) let b = exe(10) let c = exe(15) print(c)
Ans:
//Output 30