Swift Closures
Closures are self-contained blocks of functionality that can be passed around and used in code. They are similar to functions, but with a few key differences. In Swift, closures are a first-class citizen, which means they can be assigned to variables and passed as arguments to functions.
Closure Syntax
The syntax for a closure has three parts: the parameters, the "in" keyword, and the body. Here's an example of a closure that takes two integers as parameters and returns their sum:
1let add = { (a: Int, b: Int) -> Int in 2 return a + b 3}
The parameters are listed in parentheses, just like in a function. The "in" keyword separates the parameters from the body of the closure. The body of the closure is just like the body of a function, except there is no function name. Instead, the closure is assigned to a variable, which can be used to call the closure.
Using Closures
Closures can be used in many ways in Swift, such as to sort arrays, filter arrays, and map arrays. Here's an example of using a closure to sort an array of integers:
1let numbers = [5, 3, 8, 4, 1, 9, 2, 6, 7] 2let sortedNumbers = numbers.sorted { (a: Int, b: Int) -> Bool in 3 return a < b 4}
In this example, the sorted
method is called on the numbers
array, and a closure is passed as an argument. The closure takes two integers as parameters, and returns a Bool
indicating whether the first integer should come before the second integer in the sorted array. The resulting sortedNumbers
array will be [1, 2, 3, 4, 5, 6, 7, 8, 9]
.
Inferring Types
In some cases, the types of the closure parameters and return value can be inferred by the compiler. Here's an example of a closure where the types can be inferred:
1let multiply = { (a, b) in 2 return a * b 3}
In this example, the types of the a
and b
parameters are not specified, but the compiler can infer that they are both Int
. The return type of the closure is also not specified, but the compiler can infer that it is Int
.
Trailing Closures
Trailing closures are a shorthand syntax for passing a closure as the last argument to a function. Here's an example of using a trailing closure to filter an array:
1let numbers = [5, 3, 8, 4, 1, 9, 2, 6, 7] 2let evenNumbers = numbers.filter { $0 % 2 == 0 }
In this example, the filter
method is called on the numbers
array, and a trailing closure is used to determine which elements should be included in the resulting array. The closure takes a single parameter (the current element of the array), and returns a Bool
indicating whether the element should be included in the resulting array. The resulting evenNumbers
array will be [8, 4, 2, 6]
.
Capturing Values
Closures can also capture values from their surrounding context. This means that a closure can use constants and variables that are defined in the same context where the closure is defined. The closure captures a reference to the original variable or constant, rather than a copy of its value.
1func makeIncrementer(forIncrement amount: Int) -> () -> Int { 2 var runningTotal = 0 3 func incrementer() -> Int { 4 runningTotal += amount 5 return runningTotal 6 } 7 return incrementer 8} 9 10let incrementByTen = makeIncrementer(forIncrement: 10) 11print(incrementByTen()) // Output: 10 12print(incrementByTen()) // Output: 20 13print(incrementByTen()) // Output: 30 14 15let incrementBySeven = makeIncrementer(forIncrement: 7) 16print(incrementBySeven()) // Output: 7 17print(incrementBySeven()) // Output: 14
In the above example, makeIncrementer(forIncrement:)
returns a closure that increments a running total by a specified amount. The incrementer()
function is nested inside makeIncrementer(forIncrement:)
, and captures both the runningTotal
and amount
variables from its surrounding context. The runningTotal
variable persists between calls to incrementer()
, and retains its current value.
In the main part of the code, we create two instances of the closure returned by makeIncrementer(forIncrement:)
. The first closure increments the running total by 10 each time it is called, and the second closure increments the running total by 7. Each closure has its own runningTotal
variable that is independent of the other closure.
Escaping Closures
In Swift, a closure is said to escape when it is passed as an argument to a function, and is called after the function returns. When a closure is passed as an argument to a function, the function can store the closure, and call it at a later time.
1var completionHandlers: [() -> Void] = [] 2 3func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { 4 completionHandlers.append(completionHandler) 5} 6 7func someFunctionWithNonEscapingClosure(closure: () -> Void) { 8 closure() 9} 10 11class SomeClass { 12 var x = 10 13 func doSomething() { 14 someFunctionWithEscapingClosure { 15 self.x = 100 16 } 17 someFunctionWithNonEscapingClosure { 18 x = 200 19 } 20 } 21} 22 23let instance = SomeClass() 24instance.doSomething() 25print(instance.x) // Output: 200 26 27completionHandlers.first?() 28print(instance.x) // Output: 100
In the above example, the someFunctionWithEscapingClosure
function takes a closure as a parameter, and appends it to an array. The someFunctionWithNonEscapingClosure
function takes a closure as a parameter, and calls it immediately.
The SomeClass
class has a property x
, which is set to 10 by default. The doSomething()
method calls both someFunctionWithEscapingClosure
and someFunctionWithNonEscapingClosure
. The closure passed to someFunctionWithEscapingClosure
captures a reference to self
, and sets x
to 100. The closure passed to someFunctionWithNonEscapingClosure
simply sets x
to 200.