Understanding Swift Closures

Understanding Swift Closures

Intro

The Swift documentation provides a comprehensive introduction to Closures. However, in this session, we will cover some interesting aspects of Swift closures that are not fully covered in the documentation.

DEFINITION: Closures are self-contained blocks of functionality that can be passed around and used in your code.

Closures take one of three forms:

  • Global functions and class/struct/enum methods are closures that have a name and don’t capture any values.

  • Nested functions are closures that have a name and can capture values from their enclosing function.

  • Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.

NOTE: Function is a special case of closure.

NOTE: In the first two forms of closures listed above, the name actually refers to a function signature, which includes the function name and argument names.

The closure is a typed value and the type of closure is written like this:

(parameter-tuple) -> return-value

We can consider function declaration as a variable of closure type once the following are equivalent in Swift:

func isPositive(x: Int) -> Bool { return x > 0 }
let isPositive: (Int) -> Bool = { x in return x > 0 }

{ param in ... } - is called a closure expression.

Closure expressions can be considered as anonymous literals of closure type.

In the same way that 1234 and "hello" are integer and string literals. They’re also anonymous - they aren’t named. The only way they can be used is to assign them to a variable when they’re created, or to pass them to another function or method.

In the nutshell, closures provide a way to encapsulate a piece of functionality or behavior as a first-class object that can be passed around and used in different parts of your code.

How to create a reference cycle with closure?

The most dangerous thing about the closures is that you can create a reference cycle using them. Reference cycle (also known as a retain cycle) occurs when two or more objects hold strong references to each other, preventing them from being deallocated even when they're no longer needed. This can lead to memory leaks and negatively impact the performance of your app.

Let’s consider an example:

class TestObject {
    var counter = 0
    var closure: () -> Void = {}

    deinit {
        print("No reference cycles!") // will never be called
    }
}

var object = TestObject()

object.closure = {
    object.counter += 1 // reference cycle is created
}

In this example, object holds the reference to the closure and closure captures the reference to the object. Object will never be deallocated and deinit will never be called.

Now that we have identified the problem, let's explore some solutions to fix it.

class TestObject {
    var counter = 0
    var closure: () -> Void = {}

    deinit {
        print("No reference cycles!") // will be called at the end of the scope
    }
}

var object = TestObject()

object.closure = { [weak object] in // or [unowned object]
    object?.counter += 1
}

Expression [weak object] in the begining of the closure is called a capture list.

Using a capture list with a weak modifier tells the closure that the object will be captured by weak reference to avoid retain cycle creation. Let’s take a look on the capture list more closely.

What is a capture list?

When you define a closure, you can also specify a capture list, which tells the closure which variables and constants it should capture from the surrounding context.

DEFINITION: A capture list is simply a comma-separated list (or just an array) of variable or constant names, enclosed in square brackets, like this:

{ [variable1, variable2, variable3] in
    // class object variables will be captured by strong reference
    // closure code goes here
}

You can use weak or unowned modifiers to capture variables by weak or unowned reference correspondingly to prevent the retain cycle:

{ [weak variable1, unowned variable2] in
    // variable1 will be captured by weak reference
    // variable2 will be captured by unowned reference
    // closure code goes here
}

But what is the difference between weak and unowned?

Weak vs unowned

To prevent a retain cycle, you can use either the weak or unowned modifier. However, weak is generally considered safer because if the referenced object is deallocated, the weak reference becomes nil. On the other hand, an unowned reference would result in a runtime error.

Typically, unowned is used when there is a one-to-one relationship between two objects, and both are expected to be deallocated at the same time. However, using unowned is similar to using implicitly unwrapped optional. If the object is nil, a crash will occur. In general, it is recommended not to use the ! operator and this advice can be considered for unowned as well.

Even though unowned is faster and allows for immutability and non-optionality, weak is safer, and the performance gain from using unowned may not be as valuable, especially considering the risk of crashes. This is just my perspective, and ultimately, the choice of which modifier to use depends on the specific context and needs of the code.

Understanding capture lists

The concept of a capture list may appear straightforward, but there are nuances to consider.

One example is when capturing a value type in a capture list. In this case, the value type will be copied into the closure.

func returnClosureWithCapturedInteger() -> () -> Void {
    var counter = 0

    let closure = { [counter] in // capture list
        print("counter =", counter)
    }

    counter = 1
    closure() // counter = 0
    counter = 2
    closure() // counter = 0
    counter = 3

    return closure
}

let closure = returnClosureWithCapturedInteger()
closure() // counter = 0

As demonstrated, even though we captured the value of the counter and later changed it, the captured value remained equal to 0 when the closure was called. You can say that this is expected because value types are copied every time they are passed to a new location. However, what happens if we do not include the value type in the capture list?

In this case, the behavior is not as straightforward as in the previous example:

func returnClosureWithoutCapturedInteger() -> () -> Void {
    var counter = 0

    let closure = { // nothing in capture list
        print("counter =", counter)
    }

    counter = 1
    closure() // counter = 1
    counter = 2
    closure() // counter = 2
    counter = 3

    return closure
}

let closure = returnClosureWithoutCapturedInteger()
closure() // counter = 3

What happened in this case? Even though we did not capture counter in the capture list, we still used it within the closure.

In this scenario, the closure captures a reference to enclosing function context and counter inside it as well so closure can use it within its own body. By capturing context by reference, the closure ensures that the counter does not disappear after the call to returnClosureWithoutCapturedInteger ends and counter remains available the next time the returned closure() is called. This means that when we change counter later and call the closure, the updated version of counter is printed, even though it should not exist anymore as a local variable.

What about capturing reference type?

When it comes to reference types, the outcome will be the same regardless of whether you include them in the capture list or not. However, it's recommended to capture reference types in the capture list as it's the only way to resolve any reference cycles that may be created when capturing reference values. As mentioned earlier, it's advisable to always use a capture list with a weak or unowned modifier to prevent reference cycles and ensure safe memory management.

Understanding escaping/non-escaping

When closures are passed as arguments to a function, they need to be marked as @escaping to indicate the possibility of reference cycles. This annotation serves as a warning to the developer using the function. If closure is stored to be called later, it is considered escaping, while closures that don't leave the function's local scope are non-escaping. When using escaping closures, the compiler requires explicit usage of self in closure expressions to prevent unintentional strong capturing of self, which can cause reference cycles. Non-escaping closures cannot create permanent reference cycles since they are automatically destroyed when the function they're defined in returns.

By default, closures passed as function arguments are non-escaping. Remember that this default only applies to function parameters, and functions used as parameters in tuples or optionals are automatically marked as escaping. Therefore, a function that takes a function argument where the parameter is both optional and non-escaping cannot be written.

NOTE: @escaping is explicit signal of potential creating of reference cycles

Nested functions

As was already mentioned, nested functions are closures as well. This means the nested function captures variables from the surrounding context as well.

func returnNestedFunction() -> () -> Void {
    var counter = 0

    func nestedFunc() -> Void {
        print("counter =", counter)
    }

    counter = 1
    nestedFunc() // counter = 1
    counter = 2
    nestedFunc() // counter = 2
    counter = 3

    return nestedFunc
}

var nestedFunc = returnNestedFunction()
nestedFunc() // counter = 3

As you can see nested function works in the same way as closures without capture list. Nested function captures a reference to enclosing function context so when we change counter later and call the closure, the updated version of counter is printed.

Unobvious cases of retain

Capturing the object in closure not the only way you can introduce reference cycle in your code.

class AlertView {
    var buttonTapped: (() -> Void)?
}

class ViewController {
    let alert: AlertView

    init() {
        alert = AlertView()
        alert.buttonTapped = buttonTapped // self will be captured!!!
    }

    func buttonTapped() {}

    deinit {
        print("No reference cycles!") // Will never be called
    }
}

let vc = ViewController()

The alert.buttonTapped = buttonTapped line looks like an innocent assignment, but beware: we’ve just created a reference cycle! Every reference to a method of an object (like buttonTapped in the example) implicitly captures the method's self object.

When the AlertView object calls the closure that’s stored in its buttonTapped property, the property must somehow know which object’s instance method it needs to call - it’s not enough to just store a reference to ViewController.buttonTapped method without referencing the instance. If ViewController object will be deallocated at some moment AlertView object will lose access to the method. This is why it captures the self in this case.

Under the hood, instance methods are modeled as closures that, given an instance, return another closure that then operates on the instance. Implicit type of the expression ViewController.buttonTapped is actually (ViewController) -> (Int) -> ().

And vc.buttonTapped is actually just another way of writing ViewController.buttonTapped(vc).

Both expressions return a closure of type (Int) -> (), and this closure strongly captures the vc instance.

In this scenario, to avoid a strong reference retaining, you need to wrap the method call in another closure that captures the object weakly:

alert.buttonTapped = { [weak self] index in
    self?.buttonTapped(atIndex: index)
}

Is closure reference or value type?

In Swift, reference types are not limited to classes only. Functions and closures are also considered reference types.

Unlike value types, which are stored in the stack, reference types are stored in the heap. Hence, closures are also stored in the heap. Additionally, any captured value type is moved to the heap so that the closure can access it for the duration of its lifetime.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

This is an example from the Swift book and as you can already guess:

  • incrementer as a closure and it will be stored in the heap.

  • runningTotal is a value type but it will be moved to the heap as well.

Recap

  • Closures are blocks of functionality that can be passed around and used in your code;

  • Function is a special case of closure;

  • You can create a retain cycle using closures;

  • You can use capture list and weak/unowned modifiers to prevent a retain cycle.

  • Closure can catch value type by reference or copy it;

  • @escaping is an explicit signal of potential creating of reference cycles;

  • Nested functions are closures as well;

  • You should be careful with assigning a class method to closure because it retains the class object as well;

  • Closure is a reference type.

Conclusion

Overall, closures are powerful and essential tool for Swift developers, and understanding how to use them effectively can significantly reduce problems that can be introduced in your app in process of development.

Even though we discussed a lot about closures in this post, there are still a lot of things we didn’t cover that you can read about like for example multiple trailing closures(Swift 5.3) and autoclosures.

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/

Did you find this article valuable?

Support Vitaly Batrakov by becoming a sponsor. Any amount is appreciated!