Skip to main content
  1. SwiftUI in 100 Days Notes/

Day 9 - Swift Functions - 3 : What is Closure? How Do They Work? Closure Examples

Assigning Function to Variable or Constant #

We can assign functions to variables or constants, pass functions as parameters to functions, or return functions from functions.

func greetUser() {
    print("Hi there!")
}

greetUser()

var greetCopy = greetUser
greetCopy()

//OUTPUT:
//----------------------------------------
// Hi there!
// Hi there!

In the example above, a function itself is assigned to a variable.

Important : When assigning a function to a variable, () -brackets- are not used. When we use parentheses, we call the function and the return value of the function is assigned to the variable.

Defining Closure #

We saw above that the function itself can be assigned to a variable (or constant). But what if we can skip the function definition step and assign the function’s work directly to a variable? Swift calls this process closure.

let sayHello = {
    print("Hi there!")
}

sayHello()

//OUTPUT:
//----------------------------------------
// Hi there!

Above we see a function created using closure and a constant that this function is assigned to. Note that the constant is written using () parentheses to make the function call. The closure above takes no parameters and has no return value. Now let’s write a closure that contains them.

let sayHello = { (name: String) -> String in
    "Hi \(name)!"
}

closure, starts and ends with { } parentheses. Therefore, the place where we declare the parameters and return value type must be inside these parentheses. The in keyword in the example above is used to mark the end of the parameter and return type declaration.

Defining a Closure that takes no parameters and returns a value #

let payment = { () -> Bool in
    print("Paying an anonymous person…")
    return true
}

We use the empty parentheses () to indicate that Closure takes no parameters. It is quite similar to writing func payment() -> Bool.

Type of Functions #

In Swift, functions have a type, just like integers have an Int type and decimal numbers have a Double type.

Let’s assign the function greetUser(), which takes no parameters and has no return value, to the variable greetCopy using type annotation.

var greetCopy: () -> Void = greetUser

Let’s analyze the code above a bit;

  1. Empty parentheses () indicate a function that takes no parameters.
  2. -> means the same as when creating a function, i.e. we will declare the return type.
  3. Void means nothing. This means that this function has no return value. Sometimes we see () written instead of Void.

The type of each function depends on the data it receives and sends back. There is an important trick here: the names of the data it receives are not part of the function’s type. Let’s do one more example;

func getUserData(for id: Int) -> String {
    if id == 1989 {
        return "Taylor Swift"
    } else {
        return "Anonymous"
    }
}

let data: (Int) -> String = getUserData
let user = data(1989)
print(user)

Let’s assign the getUserData function, which takes data of type Int and has return type String, to the constant data. When calling the data function, instead of writing data(for: 1989), we wrote data(1989), because the function type does not contain the external parameter name.

This is also valid for closure. Let’s call the sayHello closure we defined earlier.

let sayHello = { (name: String) -> String in
    "Hi \(name)!"
}

sayHello("Taylor")
//OUTPUT:
//----------------------------------------
// Hi Taylor!

The sayHello closure does not use parameter names, just like when copying functions. So external parameter names are only important when we call a function directly, not when we create a closure or copy the function.

Using Closure #

let team = ["Gloria", "Suzanne", "Piper", "Tiffany", "Tasha"]
let sortedTeam = team.sorted()
print(sortedTeam)

The team Array will be sorted perfectly. But what if we want to control this sorting? For example, let’s say we want the team captain’s name to be first, followed by the names in alphabetical order. Here sorted() allows us to pass a special sort function to control this. This function takes two Strings as parameters and returns true if the first String is sorted first and false otherwise. If Suzanne is a captain, the code should look like this.

func captainFirstSorted(name1: String, name2: String) -> Bool {
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 < name2
}

So if name1 is Suzanne it will return true because name1 must be true because it comes before name2. But if name2 is Suzanne it will return false because name1 must be sorted after name2. If no name is Suzanne, normal alphabetical sorting with < will be done.

This way we can pass the captainFirstSorted function as a parameter to the sorted() function.

let captainFirstTeam = team.sorted(by: captainFirstSorted)
print(captainFirstTeam)
//OUTPUT:
//----------------------------------------
// ["Suzanne", "Gloria", "Piper", "Tasha", "Tiffany"]

The sorted() function requires a function that accepts two Strings and returns a Boolean. It does not matter whether this function is defined using func or is a closure.

We will rewrite the sorted() function using closure.

let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -> Bool in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 < name2
})

Let’s analyze the code above a bit;

  • We call the sorted() function as before.
  • Instead of passing a function, we write a closure.
  • We list the two String parameters that sorted() will pass to us, tell closure to return a Boolean, and mark the beginning of closure with in.
  • Everything else is normal function code.

Closure Short Syntax #

Let’s write again the code we created above a little more neatly here.

let team = ["Gloria", "Suzanne", "Piper", "Tiffany", "Tasha"]

let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -> Bool in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 < name2
})

print(captainFirstTeam)

For the sorted() function, a function that takes two Strings and returns a Boolean must be provided. So why are we repeating the same thing in closure.

To write Closure in short, we don’t need to specify the parameter and return type.

For this reason, we can write the code as follows.

let captainFirstTeam = team.sorted(by: { name1, name2 in

Now there is a way to reduce the code a bit more. Functions that accept functions like sorted() allow what is called trailing closure syntax.

let captainFirstTeam = team.sorted { name1, name2 in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 < name2
}

Thanks to the Trailing closure syntax, instead of passing closure as a parameter, we go ahead and initialize closure. In doing so, we remove the () parentheses and the parameter name of the sorted function.

There is one last step that can simplify closure in Swift. Swift can automatically provide the parameter names in closure. In this case we can use $0 and $1 instead of name1 and name2.

let captainFirstTeam = team.sorted {
    if $0 == "Suzanne" {
        return true
    } else if $1 == "Suzanne" {
        return false
    }

    return $0 < $1
}

Examples of Closure Usage #

Let’s reverse the sorting this time with the sorted() function.

let reverseTeam = team.sorted { $0 > $1 }

With the filter() function we can perform some checks on each Array element. Elements that are true as a result of these checks are returned as a new Array. For example, we can get the players in the team whose first letter is T as follows.

let tOnly = team.filter { $0.hasPrefix("T") }
print(tOnly)
//OUTPUT:
//----------------------------------------
// ["Tiffany", "Tasha"]

The map() function allows us to transform each element in the Array. It returns the transformed Array according to our request. In the example below, all elements in the Array are returned in upper case.

let uppercaseTeam = team.map { $0.uppercased() }
print(uppercaseTeam)

//OUTPUT:
//----------------------------------------
// ["GLORIA", "SUZANNE", "PIPER", "TIFFANY", "TASHA"]

Note: When working with the map() function, the type we return does not have to be the same as the type we started with. For example, we can take data as Int and return it as String.

How to Accept Functions as Parameters? #

In order to use Closure, the function must be able to take the function as a parameter. In this section we will analyze how this can be done.

We have seen the following code before;

func greetUser() {
    print("Hi there!")
}

greetUser()

var greetCopy: () -> Void = greetUser
greetCopy()

The type annotation was intentionally added when defining the greetCopy variable. Because this is exactly what we use when specifying functions as parameters. We tell Swift which parameters the function accepts and the return type.

func makeArray(size: Int, using generator: () -> Int) -> [Int] {
    var numbers = [Int]()

    for _ in 0..<size {
        let newNumber = generator()
        numbers.append(newNumber)
    }

    return numbers

Let’s examine the code above;

  1. The function name makeArray() takes two parameters, one of which is Int, and returns an Int Array.
  2. The second parameter is a function. The function itself takes no parameters, but returns an integer each time it is called.
  3. In makeArray() we create an empty Int Array and loop as many times as desired.
  4. On each iteration of the loop, we call the generator function passed as a parameter. This will return a new integer. We add this integer to the numbers Array.
  5. Finally we return the completed Array.

The most complicated part here is actually the first line:

func makeArray(size: Int, using generator: () -> Int) -> [Int] {

If we read the code above from the left;

  1. Create a new function.
  2. The name of this function is makeArray().
  3. The first parameter is size which is an Int.
  4. The second parameter is a function called generator which takes no parameters and returns an integer.
  5. As a result, makeArray() returns the Int Array.

Let’s use the function we created above

let rolls = makeArray(size: 50) {
    Int.random(in: 1...20)
}

print(rolls)

We can also write the above code without using closure as follows.

func generateNumber() -> Int {
    Int.random(in: 1...20)
}

let newRolls = makeArray(size: 50, using: generateNumber)
print(newRolls)

Functions that take more than one function as parameter #

To demonstrate this, let’s create a function that takes a function as a parameter, each of which accepts no parameters and returns nothing

func doImportantWork(first: () -> Void, second: () -> Void, third: () -> Void) {
    print("About to start first work")
    first()
    print("About to start second work")
    second()
    print("About to start third work")
    third()
    print("Done!")
}

When we want to call this function with closure, the first closure is called as before. But for the second and third closure we need to write the external parameter name and then open the closure.

doImportantWork {
    print("This is the first work")
} second: {
    print("This is the second work")
} third: {
    print("This is the third work")
}

//OUTPUT:
//----------------------------------------
//About to start first work
//Doing first thing
//About to start second work
//Doing second thing
//About to start third work
//Doing third thing
//Done!

Conclusion #

In this article we have completed the last part of Swift Functions, Closure. To summarize;

  • In Swift we can copy functions, they work the same as the original except that they lose their external parameter names.
  • All functions have a data type (like integer is Int, decimal is Double). This data type is the type of parameters they take and the return type.
  • We can create a closure by assigning to a constant or variable.
  • For closures that take parameters or return a value, this must be done in { } brackets and marked with in.
  • Functions can accept other functions as parameters. It is important to specify exactly which values the parameter functions will take and return.
    • In this case we can create a closure instead of passing a special function. Swift accepts both approaches.
  • If one or more of the last parameters of a function are functions, we can use trailing closure syntax.
  • In Closure we can use shortened parameter names like $0 and $1.

100 Days of SwiftUI Checkpoint - 5 #


You can also read this article in Turkish.
Bu yazıyı Türkçe olarak da okuyabilirsiniz.

This article contains the notes I took for myself from the articles found at SwiftUI Day 9. Please use the link to follow the original lesson.