Working with Closures in Swift

Closures

Closures are small, self-contained blocks of code that can be used in our code, not unlike a function. (The reason these are called closures is because they’re said to “close” over constants and variables.) Let’s first get some context for closures. Suppose we have an array of names that we want to sort. Well it turns out that all arrays have a sorting function that we call and pass in a function that the sorting function will use to sort the names depending on the function parameter. Really, we have two sorting functions: one that sorts and returns the sorted array, or one that does the sort in-place. This means that the sorting will modify the original array.

var names = ["Louis", "Ella", "Bill", "Freddie", "Sammy", "Kermit"]

func alphabetical(_ s1: String, _ s2: String) -> Bool {
    return s1 < s2
}

names.sort(by: alphabetical)

The sorting function requires a function whose parameters are strings and returns a boolean. That boolean should be true if the first parameter comes before the second. However, this seems like a lot of work for a really simple task! We can put the function “in-place” in a sense by using closures in place of the function when we pass it into the sorting function.

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

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.

Closures are contained in a set of braces. Any input parameters come first, surrounded in parentheses. Then comes the return arrow and a return type, if any. Then comes the in  keyword and the body of the closure. Essentially, we just copy-and-pasted the function body into the body of the closure.

However, we can make this even shorter by remembering that Swift can infer the type of the parameters and return type. The sorting function requires that the function passed in to take two strings, in this case, and return a boolean. Because of Swift’s type inference, we can get rid of the type annotations and return type from the closure. As a byproduct of this, we can also get rid of the parentheses of around the parameters.

names.sort(by: {(s1, s2) in return s1 < s2})

This even simpler than before! However, Swift can make this even simpler! Notice how our statement body is really just a single return  statement. There’s no ambiguity in this so we can omit the return  keyword.

names.sort(by: {(s1, s2) in s1 < s2})

Swift keeps making this simpler! But we’re not done yet! Swift assigns each parameter a zero-index variable like $0, $1, $2, $3, and so on for each parameter. In our case, we only have two parameters. We can use the Swift-assigned variables instead of our own to make our closure even sorter. Since our closure just has the closure body, we can omit the in  keyword as well.

names.sort(by: {$0 < $1})

We’re almost finished, but not quite! Swift strings have a specific implementation of the comparison operators, such as less-than, greater-than, equal-to, etc. In Swift, we can actually define our own operators for our particular objects, but that’s beyond the scope of this material. Instead of using the parameter names, we can very simply pass in the less-than operator and Swift will automatically infer that we want to compare the strings using the string implementation of the less-than operator.

names.sort(by: <)

This is still the second-shortest way to write this function! As it turns out, the default implementation of the sorting function will automatically assume alphabetical order so we can simply this down to a single function call with no parameters: names.sort() .

Trailing Closures

When using complicated closures in functions, it might be cleaner to write the closure as a trailing closure instead. This allows us to write the closure outside of the function call. We can only do this if the closure is the final argument, and it’s good practice to put a closure as the last parameter for this exact reason. Take the following function that takes a seed and a closure and performs an operation on a random number. We need to seed the pseudo-random number generator with a seed so that it doesn’t generate the same pseudo-random number each time. Usually we’d use the time, but we’ll just pick a number for now.

func opOnRandNum(_ seed: Int, closure: (Double) -> Double) {
    closure(Double(seed) + 17)
}

opOnRandNum(20, closure: {(num: Double) -> Double in
    var result = (num + 293) * 7
    return result - 45
})

opOnRandNum(25) {(num: Double) -> Double in
    var result = (num + 293) * 7
    return result - 45
}

The first call of the function doesn’t use a trailing closure, and we can see that since we have the external parameter label on the closure and the entire closure is inside of the parentheses of the function. The second time we call the function, we use a trailing closure. Notice how we appear to call the function with the first parameter, then we put the closure after the function call. This makes our code look much cleaner. We could actually use some of the type inference techniques we learned in the previous subsection to shorten this closure even more.

In this section, we learned about what closures are and how we can use them in our code as an alternative to very short  functions. We also used Swift’s type inference to reduce the complexity of the closure and write it in the shortest, clearest way possible. We also learned how to use trailing closures to put the closure body after the function call to make the call look cleaner.

Enumerations

An enumeration, or enum for short, is a collection of related values that we can use in a very type-safe way. For example, suppose we wanted to represent the planets somehow. We could declare integer constants for all of the planets from 1 to 8. But what if the user accidentally inputs something not in [1, 8]? Then we have to add additional error checking and this quickly becomes tedious to do a simple task: represent the planets. Enums gives us a type-safe way to do this. Let’s take a look at how.

enum Planet: Int {
    case mercury = 1
    case venus
    case earth
    case mars
    case jupiter
    case saturn
    case uranus
    case neptune
}

var home = Planet.earth
home = .mars

Above, we declare an enum with a name and several cases, or constants. Now we can create a variable and assign it to one of the constants. After we first assign the enum variable to a value for the first time, that variable can only takes values of that enum. In the last line, we change the variable, but we can only do so to another value in the same enum. As a result of this inference, we can drop the enum name and just type in a period (.) and the value.

Enums become especially useful when using them in a switch statement. Since Swift knows that the variable can only take values of a particular enum, we can drop the enum name in the case declarations, much like when we reassigned the variable above.

switch home {
case .earth:
    print("No place like home!")
case .mars:
    print("Future home maybe?")
default:
    print("Better not venture there!")
}

Though it might not look like it, this switch statement is exhaustive since our enum has a finite amount of values. Every other enum value that we didn’t have a specific case for will be in the default case.

Associated Values

Enums can not only store values, but those values can store values as well! Enums can store associated values along with the case values so that we can compact more information into an enum. Let’s think about this using an example. PNG and JPEG are two of the most widely used image formats. PNG is a lossless image format, and JPEG is a lossy image format, meaning that some image data is lost when compressing the image. Each image can have a resolution, but the JPEG should have some quality metric that tells us how lossy this image is. It would be nice if we could store information on both in case we’re converting formats. We can do exactly this with associated values.

enum Image {
    case PNG(Int)
    case JPEG(Double, Int)
}

var canihazcat = Image.PNG(1024)
canihazcat = .JPEG(1.0, 600)

The first case has an associated value of just an Int and the second has an associated value of (Double, Int). Now we can create an enum variable of a particular case and associated value. Now suppose we converted that image into a JPEG. Well we can reassign the value of the variable and the associated value. After the last line in the above snippet, the variable will fall under the case of JPEG with associated values (1.0, 600) instead of a PNG with an associated value of 1024.

We can use this with switch statements and value binding to extract those associated values. The associated values we want to extract come in parentheses with variable after the case value we’re checking for, like the following code snippet.

switch canihazcat {
case .PNG(let res):
    print("PNG with resolution \(res)ppi")
case .JPEG(let quality, let res):
    print("JPEG with \(quality*100)% quality and resolution \(res)ppi")
}

switch canihazcat {
case let .PNG(res):
    print("PNG with resolution \(res)ppi")
case let .JPEG(quality, res):
    print("JPEG with \(quality*100)% quality and resolution \(res)ppi")
}

The second switch statement is a shorthand to the first. If we’re planning on extracting all of the values, we can simply put a single let  keyword right after the case  keyword.

Raw Values

Associated values are great, but what if we want something simpler and more automatic? We can automatically assign each enum value a raw value. Every raw value must be of the same type, but we can access it and actually use it to help initialize a value. Let’s go back to our planet example.

enum Planet: Int {
    case mercury = 1
    case venus
    case earth
    case mars
    case jupiter
    case saturn
    case uranus
    case neptune
}

var homeID = Planet.earth.rawValue

To assign each enum value a raw value, we must give a “type annotation” to the enum and assign the first case value a raw value corresponding with the raw value annotation. All of the subsequent values will be assumed to go in order according to the value on the first case. We can get the actual name of the enum value by letting the raw value annotation to be of type String and Swift will infer that the raw value is the actual name of the case.

enum Planet: String {
    case mercury
    case venus
    case earth
    case mars
    case jupiter
    case saturn
    case uranus
    case neptune
}

var homeName = Planet.earth.rawValue

The raw value of Earth will now be “Earth”. We can go a step further and use the raw value to help initialize a planet. However, the associated value we provide might not be in the range, so we could get a nil value back. This is where optionals and value-binding come in handy!

var possiblePlanet = Planet(rawValue: 9)
if let pluto = possiblePlanet {
    print("Found a planet!")
} else {
    print("Pluto is not a planet anymore!")
}

To initialize a planet based on a raw value, we use the name of the enum and pass in the raw value. This returns an optional of the same type as the enum. Then we have to use value binding to see if this variable is not nil.

In this section, we learned about enumerations and how we can use them with switch statements. Enums are just collections of related values. We can assign each case value its own associated values to store more information in a case. We can also automatically assign enum cases raw values all of the same type.