Mastering Closures in Swift: A Comprehensive Guide
Closures are self-enclosing functionality that can be passed around in our code, either as constants or arguments to functions. Similar to Python’s lambdas, closures are powerful tools that enhance code organization and readability. In this guide, we’ll explore closures in-depth, understanding their various types, usage scenarios, and memory management techniques.
Trailing Closure
When the last argument passed to a function is a closure, writing the closure outside of the function’s argument parentheses is beneficial for easy readability and closures written in this manner are referred to as trailing closures.
Consider the function display
below, which takes a closure as its last argument to process data.
func display(data: String, closure : (String) -> Void){
closure(data)
}
When invoking the function above, you could either declare the closure as a trailing closure or within the argument parentheses as shown below.
// Invoking the function without using trailing closure
display(data : "Hello There", closure : { data in
print(data)
}) // Hello There
// Invoking the function with trailing closure
display(data: "Hello There"){ data in
print(data)
} // Hello There
The example above demonstrates how trailing closure could enhance readability especially when the closure expression is long.
It is important to note that a function invocation could have multiple trailing functions, invoked in the reverse order of which they were declared. The example below is used to demonstrate this feature. The function takes two integer values alongside two closures as arguments and based on which of the integer values is greater in value, the corresponding closure is called.
func allocateResource(value1 : Int, value2: Int, batch1 : () -> Void, batch2 : () -> Void){
if value1 > value2 {
batch1()
}else{
batch2()
}
}
The code below demonstrates the use of the function above.
allocateResource(value1 : 3, value2 : 7){
print("Allocate resource to Batch 1")
} batch2 : {
print("Allocate resource to Batch 2")
}
Writing functions this way helps us separate code that would handle different scenarios neatly and succinctly.
Capturing Values
When a closure is defined, it has access to the values declared within the same scope it is. The values available to a closure as a result of this are referred to as captured values and they can be used for computation within the closure as you would with arguments that are passed to functions.
The examples below demonstrate captured values in action. First, we declare a function which returns a closure.
func greet() -> (_ name : String) -> Void {
return { name in
print("Hi I am \(name)")
}
}
The return value of the greet
function can be stored in a variable as shown below
var greetPlaceholder = greet()
greetPlaceholder("John") // Hi I am John
For the next step, we would create a class Person
with a name
property as shown below:
class Person{
var name: String
init(name : String){
self.name = name
}
}
Next step, we would update the greet
function such that it would initialize an object of the Person
class and also update the closure to reference the property of this object.
func greet() -> () -> Void {
let john : Person = Person(name : "John")
return {
print("Hi, I am \(john.name)")
}
}
A call to the greetPlaceholder
would show that even after the greet
function returns, the john
object is still available in memory for use within the closure.
greetPlaceholder() // Hi I am John
Argument List
As mentioned previously, closures have access to entities declared within the same scope they exist, and these entities could be used as arguments within the closure. Although this makes it easy to work with closures, it poses the risk of memory leakage and retain-cycle in the application because all references to these entities are strong references by default.
Argument lists provide an avenue for us to define how closures handle their references with these entities. Through the use of an argument list, we can set references to entities as weak references, thereby making these entities available to us within the closures as optional data.
The example below modifies the greet function above to demonstrate the use of argument lists to manage entity references and how to use the arguments within the closure.
func greet() -> () -> Void {
let john : Person = Person(name : "John")
return { [weak john] in
if let john = john{
print("Hi, I am \(john.name)")
} else {
print("Error John object is no longer available in memory")
}
}
}
The [weak john]
part of the modified greet function is our argument list, where we configure how captured arguments are being referenced. To reference an entity with a strong reference, simply write the entity’s name within the angle bracket without adding the weak
keyword as such [john]
.
A call to the greetPlaceholder
would now show that the john
object is no longer available in memory for use within the closure once the function returns.
greetPlaceholder() // Error John object is no longer available in memory
Escaping Closures
When the lifecycle of a closure exceeds the lifecycle of the function within which it is being called, it is referred to as an escaping closure. An escaping closure is particularly useful for time-consuming operations such as network calls and file transfer. The escaping
keyword is used to declare a closure as an escaping closure.
The example below demonstrates the declaration and use of a function networkRequest
, which uses an escaping closure.
func networkRequest(data : String?, onComplete : @escaping(_ data : Bool) -> Void){
if data != nil {
onComplete(true)
} else {
onComplete(false)
}
}
// Using the function
networkRequest(data : nil){ data in
switch data{
case true:
print("Network request successful")
case false:
print("Network request failed")
}
} // Network request failed
Conclusion
Closures are a cornerstone of Swift programming, enabling modular and readable code. With trailing closures, you can streamline function calls while capturing values and argument lists help to manage context and references effectively. Escaping closures extend functionality beyond function lifecycles. By mastering closures, you unlock powerful tools that contribute to cleaner and more efficient Swift code. Keep experimenting with closures Champ, and explore their diverse applications across your software engineering journey.
Subscribe to my newsletter
Read articles from Hussein Tijani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Hussein Tijani
Hussein Tijani
Hussein Tijani is a Software Engineer based in Lagos, Nigeria. His area of expertise encompasses the entire product development cycle, API design and development and third-party integrations. He is passionate about sharing his Software Engineering knowledge through technical articles, verbal conversations and social media.