Mastering Closures in Swift: A Comprehensive Guide

Hussein TijaniHussein Tijani
5 min read

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.

12
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.