If you had a tough time understanding optionals, then you’ll probably find the prospect of mastering closures even more terrifying! Don’t worry though, they are more harmless than they look. And closures are useful, too!
This article dives into Swift closures. Closures are blocks of code that you can pass around in your code, as if you assign a function to a variable. Mastering closures is a crucial aspect of learning iOS development.
Here’s what you’ll learn:
- How closures work and how you can use them
- The syntax for declaring and calling closures
- How to compose and decompose a closure’s type
- What “closing over” means, and how to use a capture list
- How to use a closure effectively as a completion handler
Ready? Let’s go.
- How A Closure Works
- How Closure Types Work
- Closures And Type Inference
- Capturing And The Capture List
- Closures In Action: The Completion Handler
- Further Reading
You’ll have to forgive me for the cheesy tropical island image at the top of this article. For starters, it’s really hard to find a visual representation of an abstract concept like a closure. On top of that, finally “getting” closures is kinda like arriving at a tropical destination. And by the way, you’ll definitely need a happy place to endure the journey towards the mastery of closures… ;-)
How A Closure Works
Let’s look at Apple’s official description of a closure:
Closures are self-contained blocks of functionality that can be passed around and used in your code.
Said differently, a closure is a block of code that you can assign to a variable. You can then pass it around in your code, for instance to another function. The function then calls the closure and executes its code, as if the closure is an ordinary function.
As you know, variables store information in your Swift code, and functions can execute tasks. With closures, you put a function’s code in a variable, pass it around, and execute its code somewhere else.
Let’s look at an analogy:
- Bob gives Alice an instruction: wave your hands. Alice hears the instruction, and waves her hands. The hand-waving is a function.
- Alice writes her age on a piece of paper, and gives it to Bob. The piece of paper is a variable.
- Bob writes “Wave your hands!” on a piece of paper and gives it to Alice. Alice reads the instruction on the piece of paper, and waves her hands. The instruction, as passed on the piece of paper, is a closure.
Makes sense, right? Let’s write a closure in Swift!
Here’s what happens in that code:
- On the first line, you’re defining a closure and assigning it to
birthday
. The closure is the stuff between the squiggly brackets{
and}
. See how it’s similar to a function declaration? Note that the closure is assigned tobirthday
with the assignment operator=
. - On the last line, the closure is called. It’s executed by calling
birthday()
, the name of the constantbirthday
with parentheses()
. This is similar to calling a function.
At this point, the type of
birthday
, and the type of the closure, is () -> ()
. More on closure types later.
Next, let’s add a parameter to the closure. Just like functions, closures can have parameters.
Alright, that’s getting more complicated. Let’s decompose that code bit by bit. Here’s what happens:
- Just like before, we’re declaring the closure on the first line, then assign it to the constant
birthday
, and call the closure on the last line. - The closure now has one parameter of type
String
. It’s declared within the closure type(String) -> ()
. - You can then use the parameter
name
within the closure. When calling the closure, you provide a value for the parameter.
What do you make of that? In essence, there are three things that matter here:
- The closure type
(String) -> ()
- The closure expression
{ name in ... }
- The closure call
birthday(...)
Unlike Swift functions, the parameters of a closure aren’t named. When you declare a closure you can specify the types of parameters it has, such as
String
in the above example, but you don’t specify a parameter name.
In the closure expression, the
{ name in ...
part, you assign a local variable name
to the first parameter of the closure. You could have named it anything you wanted.
In fact, you could have left it out and used the
$0
shorthand! Like this:let birthday:(String) -> () = {
print("Happy birthday, \($0)!")
}
In the above code, the closure
birthday
has one parameter. Inside the closure, the shorthand $0
is used to reference the value of that parameter.
Here’s what you’ve learned so far:
- A closure is a block of code that you can pass around in your code
- Closures can have zero, one or more parameters
- Every closure has a type, including any closure parameters
Let’s take a closer look at the type of a closure, in the next section.
How Closure Types Work
Every closure has a type, just like variables.
There’s no conceptual difference between a variable that’s declared as type
Int
and a constant declared as type (Int) -> ()
. The types are different, but they both are types.
Let’s decompose closure types to better understand how closures work. In it’s most essential form, this is a closure expression:
let closureName:(ParameterTypes) -> ReturnType = { (parameterName:ParameterType) in
}
A closure with no parameters and no return type, has the following closure type:
() -> ()
The above expression consists of two empty tuples
()
and a single arrow ->
. The first tuple is the input for the closure, and the second tuple is the output for the closure.
A tuple is an ordered list of items, like
(a, b, c)
. It’s comma-separated and wrapped in parentheses. An example in Swift: let flight = (airport: "LAX, airplane: 747)
.
You can also write
Void
for the closure’s return type, like this:() -> Void
In Swift,
Void
means “nothing”. When your function returns Void
, it does not return anything. Not even nil
or an empty string! As described in the Apple Developer Documentation, Void
is an alias of an empty tuple ()
. When a function or closure doesn’t return anything, it’s return type is Void
.
You declare a closure with one parameter like this:
let birthday:(String) -> Void = { (name:String) -> Void in
...
}
That looks complicated, right? Here’s how it works:
- The stuff between the squiggly brackets
{
and}
is the closure expression. The closure assigned to the constantbirthday
with thelet
keyword. - The first
(String) -> Void
is the closure type. This closure has one parameter of typeString
and it returnsVoid
, which means “nothing”. - The second
(name:String) -> Void
is the same closure type, except that it names the first parameter of the closure asname
. It’s part of the closure expression. - The
in
keyword separates the closure parameters and the closure body. Beforein
comes the closure type, and afterin
you write the code of the closure.
Let’s look at one last example. The following function has two parameters, and returns a value:
The closure
greeting
has two parameters, both of type String
. The closure returns a value of type String
. The type of greeting
is defined explicitly, and so are the closure parameters in the closure expression itself.
When the closure is called, it is provided two arguments of type
String
, and its return value is assigned to text
, and then printed out.
See how there are two parameters of type
String
, and a return type of type String
? The closure type (String, String) -> String
defines the input and output for the closure, and the expression (time:String, name:String)
gives the input parameters local variable names.
As you’ll find out in the next chapter, you can write the exact same closure expression also like this:
let greeting:(String, String) -> String = { "Good \($0), \($1)!" }
let text = greeting("morning", "Arthur")
print(text)
Let’s summarize:
- Every closure has a type, that you define in the closure expression
- The most basic closure type is
() -> Void
, with no input parameters and no return value - You can explicitly define a closure type when declaring a variable or constant, such as
let greeting:(String) -> Void ...
- The closure expression repeats the closure type and gives every closure parameters a name, like
... { (name:String) -> Void in ...
Awesome! Let’s move on.
Closures And Type Inference
Swift has a super useful feature called type inference. When you don’t explicitly specify the type of a variable, Swift can figure out on its own what the type of that variable is. It does so based on the context of your code.
Here’s an example:
let age = 104
What’s the type of
age
? Swift will infer the type of age
based on the context of the above code. That 104
is a literal value for an integer number, so the constant age
has the type Int
.
Important: Swift is a strong-typed programming language. That means that every value has a type, even if it is inferred! Never mistake type inference for “this value has no type”. A value always has a type, you just don’t declare it explicitly. Type inference can lead to confusion. Make sure you always know the types of your variables!
Type inference and closures go hand-in-hand. As a result, you can leave out many parts of a closure expression. Swift will (usually) infer those parts on its own.
Let’s look at an example:
In the above code you’re creating an array with names, then sorting it alphabetically, then assigning the result to
sortedNames
, and then printing out the array of sorted names.
The key part of this example is
names.sorted(by: <)
. That first parameter by:
takes a closure.
Now… get a load of this. First, when we expand
<
it becomes this:names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 < s2
})
Then, that’s the same as…
names.sorted(by: { s1, s2 in return s1 < s2 } )
And it’s also exactly the same as…
names.sorted(by: { s1, s2 in s1 < s2 } )
And you can also write that like…
names.sorted(by: { $0 < $1 } )
With trailing closure syntax, you can even write it as:
names.sorted { $0 < $1 }
And finally, you can also just use the
<
operator as a function:names.sorted(by: <)
Neat, right? All thanks to type inference and closure expressions. Here’s the gist of every closure expression, starting with the most expanded one:
- The first example uses the complete closure syntax, including two parameter names
s1
ands2
of typeString
, and a return type ofBool
. It also uses thein
andreturn
keywords. - The second example omits the closure parameters, because they can be inferred from context. Since we’re sorting an array of strings, those two parameters are inferred as
String
. - The third example uses the
$0
and$1
shorthand to reference the first and second parameters of the closure. The parameters are there, they just don’t have names! Again, with inferredString
types. - The fourth example uses trailing closure syntax. When a closure is the last (or only) parameter of a function, you can write the closure outside the function’s parentheses.
- And the last example uses the smaller-than operator
<
as the closure. In Swift, operators are top-level functions. Its type is(lhs: (), rhs: ()) -> Bool
, and that fits the type ofsorted(by:)
perfectly!
It’s hopefully now that you’re starting to see the power of closures, their expressivity, and how the Swift programming language enables you to write elegant code.
It helps to practice Swift coding, and working with closures, as often as you can. Practice makes perfect! Don’t wait to learn about closures until you’ve ran into trouble. Set aside some of your time to discover and grasp new programming concepts and best practices.
Personally, I like the
sorted { $0 < $1 }
expression most. Remember that it’s not always smartest to choose the most concise programming approach. In fact, it helps to be more descriptive than clever!
By the way, closures can get crazy really quickly. Check this out:
The closure
{ $0 * $0 }
is immediately called with ()
, right after defining it, effectively assigning the result of 12 * 12
to squared
. Executing a closure directly after it is defined is the basis for lazy computed properties.
So, to summarize:
- Swift uses type inference to figure out the type of a variable or constant when it isn’t explicitly provided
- Closures and type inference go well together, so you can leave out parts of a closure expression to improve readability – but not too much
- Closure expressions can get crazy quickly, so it helps to practice using closures and play around with them
Learn how to code your own iOS apps by mastering Swift 4 and Xcode 9 » Find out how
Capturing And The Capture List
Seeing a closure as a function you can assign to a variable doesn’t do the concept of closures the justice it deserves. When I explained closures like that, I left out an important part: capturing.
The name “closure” comes from “enclosing”, and when we stretch it, it comes from the functional programming concept of “closing over”. In Swift, a closure captures variables and constants from its surrounding scope.
The words “closing over”, “capturing” and “enclosing” all mean the same thing here.
Every variable, function and closure in Swift has a scope. Scope determines where you can access a particular variable, function or closure. If a variable, function or closure isn’t in a scope, you can’t access it. Scope is sometimes called “context”.
Compare it to the “context” you define your house keys in. When you’re outside the house, and your keys are inside the house, they’re not in the same context, and you can’t unlock the front door. When both you and the house keys are outside the house, you’re in the same context, and you can get in.
Your code has global scopes and local scopes. Some examples:
- A property, as defined within a class, is part of the global class scope. Anywhere within that class you can set and get the value of the property.
- A variable, as defined in a function, is part of the local function scope. Anywhere within that function you can set and get the value of the property.
Let’s look at an example of how a closure captures its surrounding scope.
Here’s what happens in the code:
- First, a constant
name
is defined with value"Zaphod"
of typeString
. - Then, a closure is defined and assigned to
greeting
. The closure prints out some text. - Finally, the closure is executed by calling
greeting()
.
In the example, the closure closes over the local variable
name
. It encapsulates variables that are available in the scope that the closure is defined in. As a result, we can access name
even though it’s not declared locally within the closure!
A smart reader will now point out that
name
is accessible in the closure’s body because in a REPL environment, such as a sandbox or Swift Playground, top-level variables are part of the global scope. Yes, that’s right! You can access them anywhere. Please check the next example.
Let’s look at a more complex example:
What happens here?
- First, a function
addScore(_:)
is defined. It will return a score based on the parameterpoints
and the “previous” score of 42. - Then, within the function a closure
calculate
is defined. It simply addsscore
andpoints
and returns the result. The function calls the closure withcalculate()
. - Finally, the function
addScore(_:)
is called, assigned tovalue
, and printed out.
The closure
calculate
captures both score
and points
! Neither of those variables are declared locally within the closure, yet the closure can get their values. That’s because of capturing.
This color diagram explains it better visually:
See how there are different scopes? And see how the closure scope has access to
score
and points
, that are part of the local function scope?
There are a few things worth noting here:
- A closure only captures variables etc. that are actually used in the closure. When a variable isn’t accessed in the closure, it isn’t captured.
- A closure can capture variables and constants. You can technically also capture other closures, because they’re variables or constants too, and properties, because a property belongs an object that’s assigned to a variable or constant. And you can of course call functions, classes etc. in a closure, but they aren’t captured.
- Capturing only works one way. The closure captures the scope it is defined in, but code “outside” a closure doesn’t have access to values “inside” the closure.
Capturing values with closures can lead to all sorts of fun with strong reference cycles and memory leaks. And that’s where the capture list comes in.
First things first. When a closure captures a value, it automatically creates a strong reference to that value. When Bob has a strong reference to Alice, then Alice isn’t removed from memory until Bob is removed from memory.
That usually goes OK. But what if Alice has a strong reference back to Bob? Then both Bob and Alice won’t be removed from memory, because they’re holding on to each other. Bob can’t be removed because Alice is holding onto him, and Alice can’t be removed because Bob is holding onto her.
This is called a strong reference cycle and it causes a memory leak. Imagine a hundred Bob’s and Alice’s taking up 10 MB each in memory, and you see what the problem is: less memory for other apps, and no way to remove it.
Memory in iOS is managed with a concept called Automatic Reference Counting. It’s different than garbage collection. Most of memory management with ARC is done for you, but you have to avoid strong reference cycles. Memory management is a hairy subject, so we’ll leave that for another article.
You can break a strong reference cycle with a capture list. Just like you can mark a class property as
weak
, you can mark captured values in a closure as a weak
or unowned
reference. Their default is strong.
Here’s an example:
Here’s what happens in the code:
- First, you define a class
Database
. It has one propertydata
. It’s a fictional class, so imagine that it’s super memory intensive… - Then, you create an instance of
Database
, assign it todatabase
, and set itsdata
property to some integer value. - Then, you define a closure
calculate
. The closure takes one argumentmultiplier
, and it capturesdatabase
. Within the closure, thedata
is simply multiplied bymultiplier
. - Finally, the closure is called with argument
2
and its result is assigned toresult
.
The key part is the capture list, here:
... { [weak database] ...
A capture list is a comma-separated list of variable names, prepended with
weak
or unowned
, and wrapped in square brackets. Some examples:[weak self, unowned tableView]
[weak navigationController]
[unowned self, weak database]
You use a capture list to specify that a particular captured value needs to be referenced as
weak
or unowned
. Both weak
and unowned
break the strong reference cycle, so the closure won’t hold on to the captured object.
Here’s what they mean:
- The
weak
keyword indicates that the captured value can becomenil
- The
unowned
keyword indicates that the captured value never becomesnil
Both
weak
and unowned
are the opposite of a strong reference, with the difference that weak
indicates a variable that can become nil
at some point.
You typically use
unowned
when the closure and the captured value will always refer to each other, and will always be deallocated at the same time. An example is [unowned self]
in a view controller, where the closure will never outlive the view controller.
You typically use
weak
when the captured value at some point becomes nil
. This can happen when the closure outlives the context it was created in, such as a view controller that’s deallocated before a lengthy task is completed. As a result, the captured value is an optional.
The concept of capturing, capture lists and memory management is tricky. It’s not that complicated, but I think it’s just hard to visualize such an abstract concept. In practical iOS development, the most common capture list is
[weak self]
or [unowned self]
.
As you’re reading this, just take note of the concepts and refer back to them whenever you get into trouble with memory management. Xcode will tell you when you’re using
self
in a closure, and if you do, that might be a good time to read up on the exact inner workings of capture lists.
Here’s what you learned so far:
- A closure can capture its surrounding scope, making variables and constants from that scope accessible within the closure
- Variables and constants are captured with a strong reference by default, which can cause a strong reference cycle
- You can break the strong reference cycle with a capture list, by explicitly marking captured values as
weak
andunowned
Let’s move on to the next and last section about completion handlers!
Closures In Action: The Completion Handler
So what do you actually use closures for? They are incredibly powerful tools, but if you can’t put them to use, they won’t do you much good.
A common application of a closure is the completion handler. It works roughly like this:
- You’re executing a lengthy task in your code, like downloading a file, making a calculation, or waiting for a webservice request
- You want to execute some code when the lengthy task is completed, but you don’t want to “poll” the task continuously to check if it’s finished
- Instead, you provide a closure to the lengthy task, which it will call when the task is completed
Here’s an example:
Alamofire.request("http://example.com/api", completionHandler: { data in
// Do something with "data"
})
In the above code we’re making a hypothetical networking request to download some data and do something with that data when the request is completed.
That may not seem shocking, so here’s the kicker:
- We’re starting the networking request at point A
- The completion handler is executed at point B
- The completion handler can capture the scope at point A
It’s like time travel! You can code as if there’s no passing of time between starting the request and its completion. Instead of waiting for the lengthy task to complete, we can just provide a bit of code that’s executed when the task completes, while we’re coding the networking request.
When you pass a closure as an argument for a function, and when that closure outlives the function it was passed to, it is said to be an escaping closure. Since Swift 3, closures are non-escaping by default. When you declare a function that takes a closure, and when that closure is escaping, its parameter has to be marked with
@escaping
. Read more about @escaping here.
Check out this diagram:
Here’s what happens:
- A lengthy task starts, and we’re defining a completion handler. The completion handler is a closure, so it captures variables in its surrounding scope.
- The lengthy task completes, and your completion handler is executed. The closure scope is kept around, so we can use any variables and constants defined in the closure’s scope.
The key part is this: we can code the completion handler at the same time we’re starting the lengthy task!
Imagine you want to use the data of a networking request to display an image:
let imageView = UIImageView()
Alamofire.request("http://imgur.com/kittens", completionHandler: { data in
imageView.image = data
})
See what happens here? You’re defining an image view, starting the networking request, and providing a completion handler. The completion handler is executed when the lengthy task is completed.
The closure has captured a reference to
imageView
, so you can set the image data when the lengthy task is completed. This happens in the future, but we’re still in the present! You can code it as if the lengthy task executes instantly.
Let’s compare that to target-action, another common approach to invoke functions at a later point in time.
func download()
{
let imageView = UIImageView()
Alamofire.request("http://imgur.com/kittens", target: self, action: #selector(onDownloadComplete(_:)))
}
func onDownloadComplete(_ data: UIImage)
{
// Oh no! How do I get to the `imageView` from here?
}
See how that’s different?
Even though you can use target-action to determine what happens when a lengthy task completes, you don’t have the benefits of capturing, so you can’t respond to the completion in the here-and-now.
The smart reader now points out that you can make
imageView
an instance property, so you can access it in onDownloadComplete(_:)
. Yes, that’s a good alternative! But what if you need access to multiple variables? You don’t want to add unnecessary clutter.
OK, before we call it quits, let’s look at one last bit of magic with closures. Consider the following code:
Try it! What’s happening there?
- First, we define a function
lengthyTask(completionHandler:)
. When that function is executed, the completion handler is executed with one argument42
. The completion handler also returns a result, which is printed out. - Then, the function
lengthyTask(completionHandler:)
is called, and provided with a closure. The closure has one argument, as defined earlier, which is printed out withprint(number)
, and it also returns a value101
.
As a result, the first
print()
will print out 101
, and the second print()
will print out 42
. Wait a minute…
Is the closure providing a value back to the function that calls the closure? YES! It’s magical…
Because the closure is executed in
lengthyTask
, but defined earlier, you can return values from your closure just like any other function. The return value ends up with whomever called the closure!
Comments
Post a Comment