HCN DEV

한국어로 보기

Swift Closure

Swift Closure feature image

Closures in Swift, also known as anonymous functions, differ from regular functions. Instead of being declared with the func keyword, they take the form of declaring functions as variables. Closures are a powerful feature that aids in writing concise and intuitive code. Let’s explore closures by highlighting the differences in how they are used compared to regular functions.

Using Regular Functions

var counter = 0

func addCounter() {
  counter += 1
}

addCounter()
addCounter()

print(counter) // Result: 2

Regular functions are declared with a function name (addCounter in this case), and you call the function by using that name.

Using Closures

var counter = 0

let addCounter = {
    counter += 1
}

addCounter()
addCounter()

print(counter) // Result: 2

Closures, on the other hand, declare functions as variables. In this case, the addCounter variable is assigned a function, and you can call it like a function (addCounter()).

Basic Structure of a Closure

Closures in Swift have a basic structure consisting of a header and a body:

var closure = { header in body }

In the header, you specify the arguments and return type, and in the body, you write the code that gets executed when the closure is called. The in keyword separates the header from the body.

Closure Expression

The official Swift documentation by Apple explains closures using the sorted(by:) method. The sorted(by:) method is a built-in Swift method used to sort an array based on a specified criterion. It relies on the comparison of elements in the array to determine their order. In other words, it repeatedly compares two values in the array, returning true if the first value should come before the second (ascending order) or false otherwise.

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
let numbers = [4, 3, 2, 6, 1]

// The required argument depends on the data type within the array.
names.sorted(by: (String, String) -> Bool)
numbers.sorted(by: (Int, Int) -> Bool)

In this context, the by parameter of sorted(by:) expects a function or closure that returns a Bool. This means you can pass either a regular function or a closure as an argument. Here are examples of both:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// A function that returns true if s1 should come before s2.
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

var reversedNames = names.sorted(by: backward)

var reverse2 = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 < s2 })

In the above examples, reversedNames is sorted using a regular function (backward), while reverse2 is sorted using a closure.

Closure Shortening

One of the significant advantages of closures is their ability to write concise and intuitive code. However, understanding when and how to shorten closures is essential. Here, we’ll explore scenarios where you can shorten closure expressions.

1. Type Inference

Closures can infer the types of their parameters and the return type if that information is already known. This allows you to omit explicit type declarations.

names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 < s2 })

// Omitting data types
names.sorted(by: { (s1, s2) in return s1 < s2 })

For instance, in the sorted(by:) method, the closure is always expected to take two arguments of the same type as the elements in the array and return a Bool. Since this information is known, you can omit the data type declarations.

2. Omitting the “return” Keyword in Single Expression Closures

Single expression closures can omit the return keyword.

// Omitting the "return" keyword
names.sorted(by: { (s1, s2) in s1 < s2 })
var multiply: (Int, Int) -> Int = { (a, b) a * b }

In single expression closures, where the body contains only one expression, you can omit the return keyword.

3. Short-hand Argument Names

Closures provide short-hand argument names that can be used instead of explicit parameter names.

// Using short-hand argument names
names.sorted(by: { $0 < $1 })
var multiply: (Int, Int) -> Int = { $0 * $1 }

You can use $0, $1, and so on to refer to the closure’s arguments in the order they appear.

4. Operator Methods for Shortening

Operator methods can further shorten closures, especially when they involve simple operations.

names.sorted(by: <)
var multiply: (Int, Int) -> Int = (*)

In the case of sorted(by:), it always expects a closure that compares two values and returns a Bool. Similarly, the multiply closure, which multiplies two values, can be expressed concisely using the * operator.

Passing Closures as Function Arguments

Just as you can pass variables as arguments to functions, you can also pass closures to functions. The syntax is similar to passing variables, following the func functionName(label variableName: variableType) pattern.

var hello: () -> Void = { print("Hello~") }

func runClosure(name aClosure: () -> Void) {
    aClosure()
}

runClosure(name: hello) // Hello~

Utilizing Trailing Closures for Syntax Sugar

Trailing closures are a syntax sugar that allows you to separate a closure from the function call when it becomes excessively long. You can use the trailing closure syntax to make your code more readable.

// Passing arguments
runClosure(name: hello) // Hello~
runClosure(name: { print("another closure") })

runClosure() {
  // Executes when aClosure() is called
  print("trailing1")
}

// If there are no other arguments, you can omit the parentheses.
runClosure {
  print("trailing2")
}

Trailing closures can be especially useful when you have multiple arguments in a function call.

func runClosure2(index: Int, name aClosure: () -> Void) {
  aClosure()
}
runClosure2(index: 2) {
    // Passes index as 2 and executes when aClosure() is called


    print("hi")
}

Trailing closures are commonly used in libraries like Alamofire for completion handlers.

// Passing arguments
Alamofire.request(URL).responseJSON(completionHandler: { response in
  // Handle the response
  completed()
})

// Utilizing trailing closures
Alamofire.request(URL).responseJSON { response in
  // Handle the response
  completed()
})

Using the Map Method

One of the most common uses of trailing closures is with the map(_:) method. The map(_:) method is used to modify all or some of the elements in a collection. It’s like a loop, but with a strong focus on mapping values to new values.

var numbers = [4, 3, 2, 6, 1]

numbers = numbers.map { (value) -> Int in
  let newValue = value + 1
  return newValue
}

In the example above, the numbers array is modified by mapping each value to a new value (the original value plus one). The return type remains the same as the original. However, you can have a different return type if needed. You can also map collections to dictionaries or vice versa.

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]

let oddOrEvenArr = digitNames.map { (key, value) -> String in
    var str = ""
    if key % 2 == 0 {
      str = "Even"
    } else {
      str = "Odd"
    }
    return str
}
// oddOrEvenArr = ["Even", "Odd", ...] (Order may vary.)

let oddOrEvenDict = digitNames.map { (key, value) -> [Int: String] in
    var str = ""
    if key % 2 == 0 {
      str = "Even"
    } else {
      str = "Odd"
    }
    return [key: str]
}
// oddOrEvenDict = [0: "Even", 1: "Odd", ...] (Order may vary.)

In the above examples, oddOrEvenArr and oddOrEvenDict are created by mapping the digitNames dictionary into arrays and dictionaries, respectively.

References

  • Apple Inc. The Swift Programming Language (Swift 3.1)
  • Raywenderlich - Closure