Understanding Closure in Swift

A closure is a self-contained block of code that can be assigned to a constant or variable, and can be called as an argument to another function or method. In this blog post, we will explore closures in Swift and how they can be used in different scenarios.

A closure is a self-contained block of code that can be assigned to a constant or variable, and can be called as an argument to another function or method. In this blog post, we will explore closures in Swift and how they can be used in different scenarios.

In this post, there is a collection of examples that show how closures work in Swift. Let’s take a closer look.

Assign a Function to a Variable or Constant

The first example shows how to define closures as functions, and how they can be assigned to constants or variables.

var multiply = { (a: Int, b: Int) -> Int in
    return a * b
}

This defines a closure that takes two integer parameters and returns their multiplication. The closure is then assigned to a variable multiply.

// JavaScript-like
let add: (Int, Int) -> Int = {
    return $0 + $1
}

This defines another closure that takes two integer parameters and returns their sum. The closure is then assigned to a constant add. Note that the closure can also be defined as a shorthand: let add = { $0 + $1 }.

Besides, closure can also have void return value, such as

let justPrint: () -> Void = {
    print("Hello World")
}
justPrint()

Pass closures as arguments

The operate function takes two integer parameters and a closure that takes two integer parameters and returns an integer. The function calls the closure with the provided parameters and returns the result.

func operate(_ a: Int, _ b: Int, on operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
}
operate(2, 3, on: add)

This demonstrates how closures can be passed as arguments to functions.

Example: Create Counters Using Closures

The next example shows how closures can be used to create counters that keep track of the number of times they are called.

var counter = 0
let count: () -> Void  = {
    counter += 1
    print(counter)
}

for _ in 0..<10 {
    count()
}

This defines a closure count that increments a counter variable counter and prints its value. The code then creates the closure and calls it ten times.

Create Closures with State

Closures can also have state, which allows them to maintain their own set of variables and data. The example below shows how two closures that share the same state can be created.

The closure returned from this function will increment its internal counter each time it is called. Each time you call this function, you get a different counter.

func countingClosure() -> () -> Int {
    var counter = 0
    let count: () -> Int = {
        counter += 1
        return counter
    }
    return count
}

let count1 = countingClosure()
let count2 = countingClosure()
count1() // 1
count2() // 1
count1() // 2
count1() // 3
count2() // 2

The two counters created by the function are mutually exclusive and count independently. Neat!

Use Closures with Collections

Closures can be used to customize the behavior of array functions such as sorted, forEach, filter, map, reduce, and more.

let students = ["Jane", "King", "Freddie", "Steve"]
students.sorted(by: {
    $0.count > $1.count // 6 times comparison of 4 names
}) // ["Freddie", "Steve", "Jane", "King"]

let values = [1, 2, 3, 4, 5, 6]
values.forEach({
    print("\($0): \($0*$0)")
}) // 1: 1 \n 2: 4 \n 3: 9 ...

let prices = [1.5, 10, 4.99, 2.30, 8.10]
let largePrices = prices.filter({
    $0 > 5
}) // [10, 8.1]

let salePrices = prices.map({
    $0 * 0.9
}) // [1.35, 9, 4.491, 2.07, 7.29]

let array = ["1", "2", "🐶", "4", "5", "6"]
let integers = array.compactMap({
    Int($0)
}) // [1, 2, 4, 5, 6]

let nested = [["0", "1"], ["a", "b", "c"], ["🐕"]]
let flattened = nested.flatMap({ $0 }) // ["0", "1", "a", "b", "c", "🐕"]

let stock = [1.5: 5, 10: 2, 4.99: 20, 2.30: 5, 8.19: 30]
let stockSum = stock.reduce(0, {
    $0 + $1.key * Double($1.value)
}) // 384.5

These examples show how closures can be used to perform custom sorting, filtering, and mapping of arrays, as well as iterate over their elements with forEach.

Use Closures in Lazy Collections

The following example defines a isPrime function as a closure, and applied the closure inside the collection filter, to output the first 10 primes.

let isPrime: (Int) -> Bool = {
    if $0 == 1 { return false }
    if $0 == 2 || $0 == 3 { return true }
    for i in 2...Int(Double($0).squareRoot()) {
        if $0 % i == 0 { return false }
    }
    return true
}

let primes = (1...)
    .lazy
    .filter({ isPrime($0) })
    .prefix(10)

primes.forEach({ print($0) })

Use Closures for Shorthand Syntax

Call the function which calls a closure as its parameters, the closure can be write outside the parameter list as a code fragment, as illustrated.

func repeatTask(times: Int, task: () -> Void) {
    for _ in 1...10 {
        task()
    }
}

var val = 0

repeatTask(times: 10) {
    val += 1
    print("Val: \(val)")
}

Closures are a powerful feature of Swift that allow you to write more flexible and expressive code. They can be used for passing executable code as a parameter, defining local state, and customizing the behavior of higher-order functions. By using closures, you can write cleaner, more concise code that is easier to read and maintain. Understanding closures is an essential skill for any Swift developer, and this blog post provides a good introduction to using closures in different scenarios.