If you've worked with blocks in C or Objective-C or lambdas in Ruby, then you won't have a hard time wrapping your head around the concept of closures. Closures are nothing more than blocks of functionality that you can pass around in your code.
As a matter of fact, we've already worked with closures in the previous lessons. That's right: functions are closures too. Let us start with the basics and inspect the anatomy of a closure.
1. What Is a Closure?
As I said, a closure is a block of functionality that you can pass around in your code. You can pass a closure as an argument of a function or you can store it as a property of an object. Closures have many use cases.
The name closure hints at one of the key characteristics of closures. A closure captures the variables and constants of the context in which it is defined. This is sometimes referred to as closing over those variables and constants. We're going to look at value capturing in more detail at the end of this lesson.
Flexibility
You've already learned that functions can be incredibly powerful and flexible. Because functions are closures, closures are just as flexible. In this article, you discover just how flexible and powerful they are.
Memory Management
The C programming language has a similar concept, blocks. Closures in Swift, however, have a few benefits. One of the key advantages of closures in Swift is that memory management is something you, the developer, don't have to worry about.
Even retain cycles, which aren't uncommon in C or Objective-C, are handled by Swift. This reduces hard-to-find memory leaks or crashes that are caused by invalid pointers.
2. Syntax
The basic syntax of a closure isn't difficult, and it may remind you of global and nested functions, which we covered earlier in this series. Take a look at the following example.
{(a: Int) -> Int in return a + 1 }
The first thing you notice is that the entire closure is wrapped in a pair of curly braces. The parameters of the closure are wrapped in a pair of parentheses, separated from the return type by the ->
symbol. The above closure accepts one argument, a
, of type Int
, and returns an Int
. The body of the closure starts after the in
keyword.
Named closures, that is global and nested functions, look a bit different. The following example should illustrate the differences.
func increment(_ a: Int) -> Int { return a + 1 }
The most prominent differences are the use of the func
keyword and the position of the parameters and return type. A closure starts and ends with a curly brace, wrapping the parameters, return type, and closure body. Despite these differences, remember that every function is a closure. Not every closure is a function, though.
3. Closures as Parameters
Closures are powerful, and the following example illustrates how useful they can be. In the example, we create an array of states. We invoke the map(_:)
function on the array to create a new array that only contains the first two letters of each state as a capitalized string.
var states = ["California", "New York", "Texas", "Alaska"] let abbreviatedStates = states.map({ (state: String) -> String in let index = state.index(state.startIndex, offsetBy: 2) return state.substring(to: index).uppercased() }) print(abbreviatedStates)
The map(_:)
function or method is common to many programming languages and libraries, such as Ruby, PHP, and JavaScript. In the above example, the map(_:)
function is invoked on the states
array, transforms its contents, and returns a new array that contains the transformed values. Don't worry about the body of the closure for now.
Type Inference
Previously in this series, we learned that Swift is quite smart. Let me show you exactly how smart. The array of states is an array of strings. Because we invoke the map(_:)
function on the array, Swift knows that the state
argument is of type String
. This means that we can omit the type, as shown in the updated example below.
let abbreviatedStates = states.map({ (state) -> String in let index = state.index(state.startIndex, offsetBy: 2) return state.substring(to: index).uppercased() })
There are a few more things we can omit from the above example, resulting in the following one-liner.
let abbreviatedStates = states.map({ state in state.substring(to: state.index(state.startIndex, offsetBy: 2)).uppercased() })
Let me explain what's happening.
The compiler can infer that we return a string from the closure that we pass to the map(_:)
function, which means there's no reason to include it in the closure expression definition.
We can only do this if the closure's body includes a single statement, though. In that case, we can put that statement on the same line as the closure's definition, as shown in the above example. Because there's no return type in the definition and no ->
symbol preceding the return type, we can omit the parentheses enclosing the closure's parameters.
Shorthand Argument Names
It doesn't stop here, though. We can make use of shorthand argument names to simplify the above closure expression even more. When using an inline closure expression, as in the above example, we can omit the list of parameters, including the in
keyword that separates the parameters from the closure body.
In the closure body, we reference the arguments using shorthand argument names that Swift provides us with. The first argument is referenced by $0
, the second by $1
, etc.
In the updated example below, I have omitted the list of parameters and the in
keyword, and replaced the state
argument in the closure's body with the shorthand argument name $0
. The resulting statement is more concise without compromising readability.
let abbreviatedStates = states.map({ $0.substring(to: $0.index($0.startIndex, offsetBy: 2)).uppercased() })
Trailing Closures
The Swift programming language also defines a concept known as trailing closures. The idea is simple. If you pass a closure as the last argument of a function, you can place that closure outside the parentheses of the function call. The following example demonstrates how this works.
let abbreviatedStates = states.map() { $0.substring(to: $0.index($0.startIndex, offsetBy: 2)).uppercased() }
If the only argument of the function call is the closure, then it's even possible to omit the parentheses of the function call.
let abbreviatedStates = states.map { $0.substring(to: $0.index($0.startIndex, offsetBy: 2)).uppercased() }
Note that this also works for closures that contain multiple statements. In fact, that is the main reason trailing closures are available in Swift. If a closure is long or complex and it's the last argument of a function, it is often better to use the trailing closure syntax.
let abbreviatedStates = states.map { (state) -> String in let index = state.index(state.startIndex, offsetBy: 2) return state.substring(to: index).uppercased() }
4. Capturing Values
When using closures, you'll often find yourself using or manipulating constants and variables from the closure's surrounding context in the body of the closure. This is often referred to as value capturing. It simply means that a closure can capture the values of constants and variables from the context in which it is defined. Take the following example to better understand the concept of value capturing.
func changeCase(uppercase: Bool, ofStrings strings: String...) -> [String] { var newStrings = [String]() func changeToUppercase() { for s in strings { newStrings.append(s.uppercased()) } } func changeToLowerCase() { for s in strings { newStrings.append(s.lowercased()) } } if uppercase { changeToUppercase() } else { changeToLowerCase() } return newStrings } let uppercasedStates = changeCase(uppercase: true, ofStrings: "California", "New York") let lowercasedStates = changeCase(uppercase: false, ofStrings: "California", "New York")
I'm sure you agree the above example is a bit contrived, but it clearly shows how value capturing works in Swift. The nested functions, changeToUppercase()
and changeToLowercase()
, have access to the outer function's arguments, states
, as well as the newStates
variable declared in the outer function.
Let me explain what happens.
The changeCase(uppercase:ofStrings:)
function accepts a boolean as its first argument and a variadic parameter of type String
as its second parameter. The function returns an array of strings composed of the strings passed to the function as the second argument. In the function's body, we create a mutable array of strings, newStrings
, in which we store the modified strings.
The nested functions loop over the strings that are passed to the changeCase(uppercase:ofStrings:)
function and change the case of each string. As you can see, they have direct access to the strings passed to the changeCase(uppercase:ofStrings:)
function as well as the newStrings
array, which is declared in the body of the changeCase(uppercase:ofStrings:)
function.
We check the value of uppercase
, call the appropriate function, and return the newStrings
array. The two lines at the end of the example demonstrate how the changeCase(uppercase:ofStrings:)
function works.
Even though I've demonstrated value capturing with functions, remember that every function is a closure. In other words, the same rules apply to unnamed closures.
Closures
It's been mentioned several times in this article: functions are closures. There are three kinds of closures:
- global functions
- nested functions
- closure expressions
Global functions, such as the print(_:separator:terminator:)
function of the Swift standard library, capture no values. Nested functions, however, have access to and can capture the values of constants and values of the function they are defined in. The previous example illustrates this concept.
Closure expressions, also known as unnamed closures, can capture the values of constants and variables of the context they are defined in. This is very similar to nested functions.
Copying and Referencing
A closure that captures the value of a variable is able to change the value of that variable. Swift is clever enough to know whether it should copy or reference the values of the constants and variables it captures.
Developers who learn Swift and have little experience with other programming languages will take this behavior for granted. However, it's an important advantage that Swift understands how captured values are being used in a closure and, as a result, can handle memory management for us.
Conclusion
Closures are an important concept, and you will use them often in Swift. They enable you to write flexible, dynamic code that is easy both to write and to understand.
In the next article, we explore object-oriented programming in Swift, starting with objects, structures, and classes.
If you want to learn how to use Swift 3 to code advanced features for real-world apps, check out our course Go Further With Swift: Animation, Networking, and Custom Controls. Follow along with Markus Mühlberger as he codes a functional iOS weather app with live weather data, custom UI components, and some slick animations to bring everything to life.
Comments