Swift Closures Field Notes

General Notes

  • Closures are reference types
  • Global functions are closures that have names and capture no values
  • Closures are commonly used as callback functions for network API requests
  • The parameter to the .sorted() and .sort() functions accept closures--which illustrates that closures can also be used in non-asynchronous code.
  • Closures can be anonymous blocks (like lambdas in C#), they can be code blocks assigned to variables (for repeated use), or as standalone functions (also for repeated use).
  • A closure will capture any constant and variable it references from its containing scope, which has an impact on ARC reference counting and can create retain cycles if proper care isn't taken.

Returning a Closure from a Function

Closures are commonly used to return data from functions asynchronously, such as a data service that fetches information from a file system or web API.  In the following examples, the function fetchData simulates a function that makes a web API call, then returns some result via a closure.  

 1    func fetchData(withSimulatedDelay delay: Double,
 2                   completion: @escaping 
 3                     (_ average: Double, _ dataArray: [Int]) -> Void) {
 5        let numEvents = Int.random(in: 1..<10)
 6        var arr = [Int]()
 7        var total = 0.0
 9        for _ in 0..<numEvents {
10            let j = Int.random(in: 100..<1000)
11            total += Double(j)
12            arr.append(j)
13        }
15        DispatchQueue.global(qos: .background)
16                               .asyncAfter(deadline: .now() + delay) {
17            completion(total / Double(numEvents), arr)
18        }
19    }

The meaning of the @escaping decoration is discussed below, but isn't important to understand at this point.

Passing in a Closure as a Function

In this first example, the closure is a named function.

1    func closureAsFunction(average: Double, array: [Int]) {
2        print(average, array)
3    }
5    fetchData(withSimulatedDelay: 0.1, completion: closureAsFunction)

As is probably obvious, when fetchData calls completion(..), it's actually calling closureAsFunction(...).

Passing in a Closure as a named block

Next, let's define a block of code with a variable (or constant) name, and pass that to fetchData

1    let closureAsConstant: (Double, [Int]) -> Void = { average, array in
2        print(average, array)
3    }
5    fetchData(withSimulatedDelay: 0.2, completion: closureAsConstant)

Closures as functions or blocks can be very useful when the blocks are well defined in advance and will be re-used many times (or even twice). While the next examples (unnamed blocks) are more commonly used, don't forget about these first two alternatives. They can keep code more modular and may often support DRY (Don't Repeat Yourself) principals in your design.

Passing in a Closure as an unnamed block

Closures don't have to be defined as functions or assigned to variable names, but instead can be unnamed blocks. These are similar to lambdas in other languages.

1    fetchData(withSimulatedDelay: 0.5,
2              completion: { (average : Double, array : [Int]) in
3       print(average, array)
4    })

fetchData calls the block via its parameter named completion.  Think of the block ({..}) as an inner function that doesn't have a name and is coded at the point it's used.

Trailing Closures

If a closure is the last parameter of a function's signature, and an unnamed block is the closure, then closure's parameter name can be omitted.  This call has the same effect as the previous one.

1    fetchData(withSimulatedDelay: 0.5) { (average : Double, 
2                                          array : [Int]) in
3        print(average, array)
4    }

Swift 5.3+ includes support for multiple trailing closures. See below for examples.

Syntax Variants for Trailing Closures

 1    // Include return types. Helpful to avoid confusion of return types
 2    fetchData(withSimulatedDelay: 0.5) { (average : Double, array : [Int]) in
 3       print(average, array)
 4    }
 6    // But the types are optional, since fetchData has already
 7    // told the compiler what the types should be
 8    fetchData(withSimulatedDelay: 1.0) { (average, array) in
 9       print(average, array)
10    }
12    // The parends around the return parameters are 
13    // usually not required
14    fetchData(withSimulatedDelay: 2.0) { average, array in
15       print(average, array)
16    }
18    // Unused parameters can be replaced with underscore
19    fetchData(withSimulatedDelay: 2.0) { _, array in
20       print(array)
21    }

Using typealias for Closure Signatures

It can sometimes be be useful to use typealias to define the parameters returned to closures.

Useful Scenarios

  • If the parameters are numerous, the typealias can reduce repetitive typing
  • The typealias can increase readability of code by having less syntax in the  signature for functions accepting closure parameters.
  • When many functions accept closure parameters having the same signature, defining the parameter list in a typealias can be helpful. Since changes to closure parameter lists are defined centrally, a refactor of the parameters only needs to be changed in one place.

First, define the closure signature as a typealias

1    typealias DataServiceResponse = (Double, [Int]) -> Void

Then any function that returns data to closures with this signature can use the typealias in the parameter list rather than the full closure signature. Here's fetchData defined with the typealias*:*

1    func fetchData(withSimulatedDelay delay: Double,
2                   completion: @escaping DataServiceResponse) {
3        .
4        .
5        .
6    }

Pass the closure to fetchData identically as before

1    fetchData(withSimulatedDelay: 1.0) { average, array in
2       print(average, array)
3    }

Multiple Trailing Closures

Prior to Swift 5.3, only the last closure could be defined with the trailing syntax (i.e. excluding the closure parameter name).  From Swift 5.3, trailing closures can be stacked.

Keep in mind that trailing closures will not be marked with names. If your code would be confusing to read with multiple trailing closures, you can still use named closure blocks as before.

First, you need to define a function that accepts a list of closures as final parameters:

 1    func fetchData(withSimulatedDelay delay: Double,
 2       arrayResponse: @escaping (_ average: Double, _ array: [Int]) -> Void,
 3       setResponse: @escaping (_ set: Set<Int>) -> Void,
 4       dictResponse: @escaping (_ dictionary: [Int:Int]) -> Void) {
 6        let numEvents = Int.random(in: 1..<10)
 8        var arr = [Int]()
 9        var set = Set<Int>()
10        var dict = [Int:Int]()
11        var total = 0.0
13        for _ in 0..<numEvents {
14            let j = Int.random(in: 100..<1000)
15            total += Double(j)
16            arr.append(j)
17            if !set.contains(j) {
18                set.insert(j)
19            }
20            dict[j] = 1
21        }
23        DispatchQueue.global(qos: .background)
24                         .asyncAfter(deadline: .now() + delay) {
25            // Call each of the closures
26            arrayResponse(total / Double(numEvents), arr)
27            setResponse(set)
28            dictResponse(dict)
29        }
30    }

Then call the function, and list each closure block one after the other

1    fetchData(withSimulatedDelay: 0.5) { average, array in
2            print(average, array)
3        } setResponse: { set in
4            print(set.sorted(by: >))
5        } dictResponse: { dict in
6            print(dict.values)
7        }

Each closure block above is independent of the others, and there's no reason to expect they all return at the same time.  fetchData could make three separate web API requests, with the 2nd or 3rd returning data from the remote web server much later than the 1st. The closure for each is called whenever fetchData calls each closure.

Escaping Closures

If a closure is called by the function it's passed to after that function has ended, the closure is called escaping. These types of closures are very common when using closures as call-back functions for asynchronous operations such as web API requests or file I/O requests.  

In fetchData, the DispatchQueue..asyncAfter ensures that the closure will be called after fetchData has returned. Its closure is therefore escaping fetchData.

Functions that may accept escaping closures are decorated with @escaping at the point the closure parameter is defined:

1    func fetchData(completion: @escaping (_ average: Double, 
2                                          _ events: [Int]) -> Void)

If the compiler sees that fetchData is designed to call a closure after it returns (e.g. on an asynchronous thread), a compiler error will be issued.

Weak/Unowned Self

When passing an @escaping closure to another object, care should be taken to avoid allowing the closure to unnecessarily hold a retain lock on the object that created it.  Doing so may delay deinit of the object that created the closure object (in the best case), and could create retain cycles that lead to memory leaks (in the worse case).

But don't panic – avoiding these scenarios is usually easy.

A captured variable is declared weak via the* [weak variableName]* decoration. A variable so decorated will be redefined as optional within the closure block, and the reference to it from within the closure block will not contribute to its retain count. This will help avoid excessively long ARC locks, retention cycles and/or memory leaks.

  • The closure should then verify any weak captured variable is not nil before accessing it.

  • Marking captured variables weak is almost always a good idea when passing a closure as an @escaping closure.

  • In the following code, the weak self notation turns self into an optional, and tells the compiler that the block shouldn't increment the retain count of self.

  • Checking for non-nil of the (now optional) self within the block avoids a crash in case self was destroyed before the block was called.

1    fetchData(completion: { [weak self] (array : [Int]) in
2                            print("self is nil? \(self == nil)")
3                            self?.workDone = true
4                            print(array)
5                          })

[unowned self] is another option, which has the same effect on retain count, but doesn't redefine the captured variable as optional within the closure block. Keep in mind, though, that an [unowned self] variable could become nil if its retain count goes to zero before the closure block is called, so checking for nil may still needed when this alternative is used.

[unowned self] is a bit like telling the compiler "trust me, I've got this"