Day 09
Day 09 êŽë š
Closures
Brace yourself, because today weâre covering the first thing in Swift that many people have a hard time understanding. Please keep in mind Flip Wilson's law: âyou can't expect to hit the jackpot if you don't put a few nickels in the machine.â
Today the topic is closures, which are a bit like anonymous functions â functions we can create and assign directly to a variable, or pass into other functions to customize how they work. Yes, you read that right: passing one function into another as a parameter.
Closures are really difficult. Iâm not saying that to put you off, only so that you know in advance if youâre finding closures hard to understand or hard to remember, itâs okay â weâve all been there!
Sometimes the syntax for closures can be a bit hard on your eyes, and this will really be apparent as you work through todayâs lessons. If you find it a bit overwhelming â if youâre staring at some code and arenât 100% sure of what it means â just go back one video and watch it again to give your memory a little nudge. Youâll find there are more tests and optional reading links than usual below, hopefully helping to solidify your knowledge.
SwiftUI uses closures extensively so itâs worth taking the time to understand whatâs going on here. Yes, closures are probably the most complex feature of Swift, but itâs a bit like cycling up a hill â once youâve reached the top, once youâve mastered closures, it all gets much easier.
Today you have three tutorials to follow, plus a summary and another checkpoint. As always once youâve completed each video thereâs some optional extra reading and short tests to help make sure youâve understood what was taught. This time youâll notice thereâs quite a bit of each of those because closures really can take some time to understand, so donât be afraid to explore!
1. How to create and use closures
1. How to create and use closures
Functions are powerful things in Swift. Yes, youâve seen how you can call them, pass in values, and return data, but you can also assign them to variables, pass functions into functions, and even return functions from functions.
For example:
func greetUser() {
print("Hi there!")
}
greetUser()
var greetCopy = greetUser
greetCopy()
That creates a trivial function and calls it, but then creates a copy of that function and calls the copy. As a result, it will print the same message twice.
Important: When youâre copying a function, you donât write the parentheses after it â itâs var greetCopy = greetUser
and not var greetCopy = greetUser()
. If you put the parentheses there you are calling the function and assigning its return value back to something else.
But what if you wanted to skip creating a separate function, and just assign the functionality directly to a constant or variable? Well, it turns out you can do that too:
let sayHello = {
print("Hi there!")
}
sayHello()
Swift gives this the grandiose name closure expression, which is a fancy way of saying we just created a closure â a chunk of code we can pass around and call whenever we want. This one doesnât have a name, but otherwise itâs effectively a function that takes no parameters and doesnât return a value.
If you want the closure to accept parameters, they need to be written in a special way. You see, the closure starts and ends with the braces, which means we canât put code outside those braces to control parameters or return value. So, Swift has a neat workaround: we can put that same information inside the braces, like this:
let sayHello = { (name: String) -> String in
"Hi \(name)!"
}
I added an extra keyword there â did you spot it? Itâs the in
keyword, and it comes directly after the parameters and return type of the closure. Again, with a regular function the parameters and return type would come outside the braces, but we canât do that with closures. So, in
is used to mark the end of the parameters and return type â everything after that is the body of the closure itself. Thereâs a reason for this, and youâll see it for yourself soon enough.
In the meantime, you might have a more fundamental question: âwhy would I ever need these things?â I know, closures do seem awfully obscure. Worse, they seem obscure and complicated â many, many people really struggle with closures when they first meet them, because they are complex beasts and seem like they are never going to be useful.
However, as youâll see this gets used extensively in Swift, and almost everywhere in SwiftUI. Seriously, youâll use them in every SwiftUI app you write, sometimes hundreds of times â maybe not necessarily in the form you see above, but youâre going to be using it a lot.
To get an idea of why closures are so useful, I first want to introduce you to function types. Youâve seen how integers have the type Int
, and decimals have the type Double
, etc, and now I want you to think about how functions have types too.
Letâs take the greetUser()
function we wrote earlier: it accepts no parameters, returns no value, and does not throw errors. If we were to write that as a type annotation for greetCopy
, weâd write this:
var greetCopy: () -> Void = greetUser
Letâs break that downâŠ
- The empty parentheses marks a function that takes no parameters.
- The arrow means just what it means when creating a function: weâre about to declare the return type for the function.
Void
means ânothingâ â this function returns nothing. Sometimes you might see this written as()
, but we usually avoid that because it can be confused with the empty parameter list.
Every functionâs type depends on the data it receives and sends back. That might sound simple, but it hides an important catch: the names of the data it receives are not part of the functionâs type.
We can demonstrate this with some more code:
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)
That starts off easily enough: itâs a function that accepts an integer and returns a string. But when we take a copy of the function the type of function doesnât include the for
external parameter name, so when the copy is called we use data(1989)
rather than data(for: 1989)
.
Cunningly this same rule applies to all closures â you might have noticed that I didnât actually use the sayHello
closure we wrote earlier, and thatâs because I didnât want to leave you questioning the lack of a parameter name at the call site. Letâs call it now:
sayHello("Taylor")
That uses no parameter name, just like when we copy functions. So, again: external parameter names only matter when weâre calling a function directly, not when we create a closure or when we take a copy of the function first.
Youâre probably still wondering why all this matters, and itâs all about to become clear. Do you remember I said we can use sorted()
with an array to have it sort its elements?
It means we can write code like this:
let team = ["Gloria", "Suzanne", "Piper", "Tiffany", "Tasha"]
let sortedTeam = team.sorted()
print(sortedTeam)
Thatâs really neat, but what if we wanted to control that sort â what if we always wanted one person to come first because they were the team captain, with the rest being sorted alphabetically?
Well, sorted()
actually allows us to pass in a custom sorting function to control exactly that. This function must accept two strings, and return true if the first string should be sorted before the second, or false if the first string should be sorted after the second.
If Suzanne were the captain, the function would 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 the first name is Suzanne, return true so that name1
is sorted before name2
. On the other hand, if name2
is Suzanne, return false so that name1
is sorted after name2
. If neither name is Suzanne, just use <
to do a normal alphabetical sort.
Like I said, sorted()
can be passed a function to create a custom sort order, and as long as that function a) accepts two strings, and b) returns a Boolean, sorted()
can use it.
Thatâs exactly what our new captainFirstSorted()
function does, so we can use it straight away:
let captainFirstTeam = team.sorted(by: captainFirstSorted)
print(captainFirstTeam)
When that runs it will print ["Suzanne", "Gloria", "Piper", "Tasha", "Tiffany"]
, exactly as we wanted.
Weâve now covered two seemingly very different things. First, we can create closures as anonymous functions, storing them inside constants and variables:
let sayHello = {
print("Hi there!")
}
sayHello()
And weâre also able to pass functions into other functions, just like we passed captainFirstSorted()
into sorted()
:
let captainFirstTeam = team.sorted(by: captainFirstSorted)
The power of closures is that we can put these two together: sorted()
wants a function that will accept two strings and return a Boolean, and it doesnât care if that function was created formally using func
or whether itâs provided using a closure.
So, we could call sorted()
again, but rather than passing in the captainFirstTeam()
function, instead start a new closure: write an open brace, list its parameters and return type, write in
, then put our standard function code.
This is going to hurt your brain at first. Itâs not because youâre not smart enough to understand closures or not cut out for Swift programming, only that closures are really hard. Donât worry â weâre going to look at ways to make this easier!
Okay, letâs write some new code that calls sorted()
using a 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
})
Thatâs a big chunk of syntax all at once, and again I want to say itâs going to get easier â in the very next chapter weâre going to look at ways to reduce the amount of code so itâs easier to see whatâs going on.
But first I want to break down whatâs happening there:
- Weâre calling the
sorted()
function as before. - Rather than passing in a function, weâre passing a closure â everything from the opening brace after
by:
down to the closing brace on the last line is part of the closure. - Directly inside the closure we list the two parameters
sorted()
will pass us, which are two strings. We also say that our closure will return a Boolean, then mark the start of the closureâs code by usingin
. - Everything else is just normal function code.
Again, thereâs a lot of syntax in there and I wouldnât blame you if you felt a headache coming on, but I hope you can see the benefit of closures at least a little: functions like sorted()
let us pass in custom code to adjust how they work, and do so directly â we donât need to write out a new function just for that one usage.
Now you understand what closures are, letâs see if we can make them easier to readâŠ
1. How to create and use closures - Additional
- Optional: What the heck are closures and why does Swift love them so much?
- Optional: Why are Swiftâs closure parameters inside the braces?
- Optional: How do you return a value from a closure that takes no parameters?
- Test: Creating basic closures
- Test: Accepting parameters in a closure
- Test: Returning values from a closure
2. How to use trailing closures and shorthand syntax
2. How to use trailing closures and shorthand syntax
Swift has a few tricks up its sleeve to reduce the amount of syntax that comes with closures, but first letâs remind ourselves of the problem. Hereâs the code we ended up with at the end of the previous chapter:
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)
If you remember, sorted()
can accept any kind of function to do custom sorting, with one rule: that function must accept two items from the array in question (thatâs two strings here), and return a Boolean set to true if the first string should be sorted before the second.
To be clear, the function must behave like that â if it returned nothing, or if it only accepted one string, then Swift would refuse to build our code.
Think it through: in this code, the function we provide to sorted()
must provide two strings and return a Boolean, so why do we need to repeat ourselves in our closure?
The answer is: we donât. We donât need to specify the types of our two parameters because they must be strings, and we donât need to specify a return type because it must be a Boolean. So, we can rewrite the code to this:
let captainFirstTeam = team.sorted(by: { name1, name2 in
Thatâs already reduced the amount of clutter in the code, but we can go a step further: when one function accepts another as its parameter, like sorted()
does, Swift allows special syntax called trailing closure syntax. It looks like this:
let captainFirstTeam = team.sorted { name1, name2 in
if name1 == "Suzanne" {
return true
} else if name2 == "Suzanne" {
return false
}
return name1 < name2
}
Rather than passing the closure in as a parameter, we just go ahead and start the closure directly â and in doing so remove (by:
from the start, and a closing parenthesis at the end. Hopefully you can now see why the parameter list and in
come inside the closure, because if they were outside it would look even weirder!
Thereâs one last way Swift can make closures less cluttered: Swift can automatically provide parameter names for us, using shorthand syntax. With this syntax we donât even write name1, name2 in
any more, and instead rely on specially named values that Swift provides for us: $0
and $1
, for the first and second strings respectively.
Using this syntax our code becomes even shorter:
let captainFirstTeam = team.sorted {
if $0 == "Suzanne" {
return true
} else if $1 == "Suzanne" {
return false
}
return $0 < $1
}
I left this one to last because itâs not as clear cut as the others â some people see that syntax and hate it because itâs less clear, and thatâs okay.
Personally I wouldnât use it here because weâre using each value more than once, but if our sorted()
call was simpler â e.g., if we just wanted to do a reverse sort â then I would:
let reverseTeam = team.sorted {
return $0 > $1
}
So, in
is used to mark the end of the parameters and return type â everything after that is the body of the closure itself. Thereâs a reason for this, and youâll see it for yourself soon enough.
There Iâve flipped the comparison from <
to >
so we get a reverse sort, but now that weâre down to a single line of code we can remove the return
and get it down to almost nothing:
let reverseTeam = team.sorted { $0 > $1 }
There are no fixed rules about when to use shorthand syntax and when not to, but in case itâs helpful I use shorthand syntax unless any of the following are true:
- The closureâs code is long.
$0
and friends are used more than once each.- You get three or more parameters (e.g.
$2
,$3
, etc).
If youâre still unconvinced about the power of closures, letâs take a look at two more examples.
First up, the filter()
function lets us run some code on every item in the array, and will send back a new array containing every item that returns true for the function. So, we could find all team players whose name begins with T like this:
let tOnly = team.filter { $0.hasPrefix("T") }
print(tOnly)
That will print ["Tiffany", "Tasha"]
, because those are the only two team members whose name begins with T.
And second, the map()
function lets us transform every item in the array using some code of our choosing, and sends back a new array of all the transformed items:
let uppercaseTeam = team.map { $0.uppercased() }
print(uppercaseTeam)
That will print ["GLORIA", "SUZANNE", "PIPER", "TIFFANY", "TASHA"]
, because it has uppercased every name and produced a new array from the result.
Tip: When working with map()
, the type you return doesnât have to be the same as the type you started with â you could convert an array of integers to an array of strings, for example.
Like I said, youâre going to be using closures a lot with SwiftUI:
- When you create a list of data on the screen, SwiftUI will ask you to provide a function that accepts one item from the list and converts it something it can display on-screen.
- When you create a button, SwiftUI will ask you to provide one function to execute when the button is pressed, and another to generate the contents of the button â a picture, or some text, and so on.
- Even just putting stacking pieces of text vertically is done using a closure.
Yes, you can create individual functions every time SwiftUI does this, but trust me: you wonât. Closures make this kind of code completely natural, and I think youâll be amazed at how SwiftUI uses them to produce remarkably simple, clean code.
2. How to use trailing closures and shorthand syntax - Additional
3. How to accept functions as parameters
3. How to accept functions as parameters
Thereâs one last closure-related topic I want to look at, which is how to write functions that accept other functions as parameters. This is particularly important for closures because of trailing closure syntax, but itâs a useful skill to have regardless.
Previously we looked at this code:
func greetUser() {
print("Hi there!")
}
greetUser()
var greetCopy: () -> Void = greetUser
greetCopy()
Iâve added the type annotation in there intentionally, because thatâs exactly what we use when specifying functions as parameters: we tell Swift what parameters the function accepts, as well its return type.
Once again, brace yourself: the syntax for this is a little hard on the eyes at first! Hereâs a function that generates an array of integers by repeating a function a certain number of times:
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 break that downâŠ
- The function is called
makeArray()
. It takes two parameters, one of which is the number of integers we want, and also returns an array of integers. - The second parameter is a function. This accepts no parameters itself, but will return one integer every time itâs called.
- Inside
makeArray()
we create a new empty array of integers, then loop as many times as requested. - Each time the loop goes around we call the
generator
function that was passed in as a parameter. This will return one new integer, so we put that into thenumbers
array. - Finally the finished array is returned.
The body of the makeArray()
is mostly straightforward: repeatedly call a function to generate an integer, adding each value to an array, then send it all back.
The complex part is the very first line:
func makeArray(size: Int, using generator: () -> Int) -> [Int] {
There we have two sets of parentheses and two sets of return types, so it can be a bit of a jumble at first. If you split it up you should be able to read it linearly:
- Weâre creating a new function.
- The function is called
makeArray()
. - The first parameter is an integer called
size
. - The second parameter is a function called
generator
, which itself accepts no parameters and returns an integer. - The whole thing â
makeArray()
â returns an array of integers.
The result is that we can now make arbitrary-sized integer arrays, passing in a function that should be used to generate each number:
let rolls = makeArray(size: 50) {
Int.random(in: 1...20)
}
print(rolls)
And remember, this same functionality works with dedicated functions too, so we could write something like this:
func generateNumber() -> Int {
Int.random(in: 1...20)
}
let newRolls = makeArray(size: 50, using: generateNumber)
print(newRolls)
That will call generateNumber()
50 times to fill the array.
While youâre learning Swift and SwiftUI, there will only be a handful of times when you need to know how to accept functions as parameters, but at least now you have an inkling of how it works and why it matters.
Thereâs one last thing before we move on: you can make your function accept multiple function parameters if you want, in which case you can specify multiple trailing closures. The syntax here is very common in SwiftUI, so itâs important to at least show you a taste of it here.
To demonstrate this hereâs a function that accepts three function parameters, each of which accept no parameters and return 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!")
}
Iâve added extra print()
calls in there to simulate specific work being done in between first
, second
, and third
being called.
When it comes to calling that, the first trailing closure is identical to what weâve used already, but the second and third are formatted differently: you end the brace from the previous closure, then write the external parameter name and a colon, then start another brace.
Hereâs how that looks:
doImportantWork {
print("This is the first work")
} second: {
print("This is the second work")
} third: {
print("This is the third work")
}
Having three trailing closures is not as uncommon as you might expect. For example, making a section of content in SwiftUI is done with three trailing closures: one for the content itself, one for a head to be put above, and one for a footer to be put below.
3. How to accept functions as parameters - Additional
4. Summary: Closures
4. Summary: Closures
Weâve covered a lot about closures in the previous chapters, so letâs recap:
- You can copy functions in Swift, and they work the same as the original except they lose their external parameter names.
- All functions have types, just like other data types. This includes the parameters they receive along with their return type, which might be
Void
â also known as ânothingâ. - You can create closures directly by assigning to a constant or variable.
- Closures that accept parameters or return a value must declare this inside their braces, followed by the keyword
in
. - Functions are able to accept other functions as parameters. They must declare up front exactly what data those functions must use, and Swift will ensure the rules are followed.
- In this situation, instead of passing a dedicated function you can also pass a closure â you can make one directly. Swift allows both approaches to work.
- When passing a closure as a function parameter, you donât need to explicitly write out the types inside your closure if Swift can figure it out automatically. The same is true for the return value â if Swift can figure it out, you donât need to specify it.
- If one or more of a functionâs final parameters are functions, you can use trailing closure syntax.
- You can also use shorthand parameter names such as
$0
and$1
, but I would recommend doing that only under some conditions. - You can make your own functions that accept functions as parameters, although in practice itâs much more important to know how to use them than how to create them.
Of all the various parts of the Swift language, Iâd say closures are the single toughest thing to learn. Not only is the syntax a little hard on your eyes at first, but the very concept of passing a function into a function takes a little time to sink in.
So, if youâve read through these chapters and feel like your head is about to explode, thatâs great â it means youâre half way to understanding closures!
5. Checkpoint 5
5. Checkpoint 5
With closures under your belt, itâs time to try a little coding challenge using them.
Youâve already met sorted()
, filter()
, map()
, so Iâd like you to put them together in a chain â call one, then the other, then the other back to back without using temporary variables.
Your input is this:
let luckyNumbers = [7, 4, 38, 21, 16, 15, 12, 33, 31, 49]
Your job is to:
- Filter out any numbers that are even
- Sort the array in ascending order
- Map them to strings in the format â7 is a lucky numberâ
- Print the resulting array, one item per line
So, your output should be as follows:
7 is a lucky number
15 is a lucky number
21 is a lucky number
31 is a lucky number
33 is a lucky number
49 is a lucky number
If you need hints they are below, but honestly you should be able to tackle this one either from memory or by referencing recent chapters in this book.
Still here? Okay, here are some hints:
- You need to use the
filter()
,sorted()
, andmap()
functions. - The order you run the functions matters â if you convert the array to a string first,
sorted()
will do a string sort rather than an integer sort. That means 15 will come before 7, because Swift will compare the â1â in â15â against â7â. - To chain these functions, use
luckyNumbers.first { }.second { }
, obviously putting the real function calls in there. - You should use
isMultiple(of:)
to remove even numbers.
Thatâs another key Swift topic under your belt â good job! Now do the smart thing and post your progress online: it forces you to write about these things in your own words, and also encourages you to continue on to tomorrow.