If you’ve read the Beginner’s Guide to Swift Functions article, you’d know that functions are one way to group code that executes together.
In this article, we’ll explore closures
, an alternative way of grouping code. We’ll discuss what closures
are, its difference to functions
, and when best to use it.
Before we begin, I would recommend giving the Beginner’s Guide to Swift Functions a few minutes to read. It tackles concepts such as the function signature, function body, parameter arguments, and return type in greater detail. These concepts are discussed here as well but in lesser detail.
Swift Closures
Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages. – Swift Programming Language Guide (Swift 5.8, 2023)
A Closure in Swift is basically a block of code that are executed together without creating a named function.
Closure Expression Syntax
{ (<parameters>) -> <#return type> in }
The closure expression syntax looks like a function that doesn’t have a name in its signature. If you recall from the Beginner’s Guide to Swift Functions, a function body is written inside the curly braces {}
, and closures start with just that, curly braces {}
.
There are subtle differences though, closures are not declared using the func
keyword, and they don’t have names in their signatures.
Closure Signature
Similar to functions, a closure can accept a list of parameters, and return a value. Closure signatures look like these:
// A closure that accepts two String parameters // and returns a Boolean (String, String) -> Bool // A closure that accepts a number and returns a number (Int) -> Int // Example of a closure that accepts no parameter, // and doesn't return a value. () -> Void
It’s hard to determine what a closure does based on its signature alone since it doesn’t have a name that tells us what it does, but its signature does tell what parameters it accepts as well as its return type.
The way that we can infer what a closure does is by reading the code inside its body.
Following the closure expression syntax above, let’s implement some examples of how to use them based on their signatures.
// Example 1: (String, String) -> Bool closure { (string1: String, string2: String) -> Bool in // we can implement this closure to determine // if the characters from the first string (string1) // appears first in the alphabet // than the characters from string2 return string1 < string2 } // As you might've guessed a use-case for the closure // above is for sorting an array of strings // alphabetically in ascending order // If we want to reverse this behaviour, like sorting // by descending order, we can change its operator // from < (less than) to > (greater than). // Example 2: (Int) - Int closure // Let's define the (Int) -> Int closure: { (number: Int) -> Int in // Let's imagine that we're creating a closure that // squares the input number and returns it return pow(number, 2) } // Example 3: () -> Void closure // For this closure, we'll print a simple statement! { in print("Closure is executed") }
The examples above show you how to implement closures but with a single line of code. Since closures are also a way to group code, you can write multiple lines of code inside the closure as long as you satisfy its return type.
Walkthrough of the Syntax
A closure starts and end with curly braces {}
. When defining a closure expression, you need to define the parameters and return type right after the opening brace {
and end it with the keyword in
.
The in
keyword precedes the group of code that you’ll want to execute inside the closure. You can have a single line of code as shown in the examples above, or you can write multiple lines of code depending on your needs. What’s important is that you’ll return
a value that matches the return type of the closure, unless the closure expects to return a Void
where a return
statement is unnecessary.
Using Closures
Calling functions are easy since functions have names, but closures don’t have names, so how do we call them?
Closures are typically used alongside functions. An example of this would be the Array.sorted(by:)
method. The sorted(by:)
function takes another function as a parameter, that’ll determine if a values are to be sorted in ascending
or descending
manner.
If you remember from the closure examples above, we have implemented a closure that’ll determine if the first string’s characters appear first before the characters from the second string. We can use that closure to sort an array of strings in alphabetical order.
let fruits = ["Banana", "Coconut", "Apple"] var sortedFruits = fruits.sorted(by: { (string1: String, string2: String) -> Bool in return string1 < string2 }) // sortedFruits will now have: // ["Apple", "Banana", "Coconut"]
Here we’ve managed to use a closure to help the sorted(by:)
function implement its sorting behaviour.
However, as you’ve might notice, writing closures in full closure expression is tedious. Could we make this code shorter?
Yes, we can!
Inferring Type From Context
A closure’s type can be inferred from the function signature that calls it.
Expanding from the Array.sorted(by:)
example above, the function signature of the sorted(by:)
function when specialized to an Array of Strings will be:
func sorted(by: (String, String) -> Bool)) -> [String]
With that, the compiler can infer that any closures passed into the sorted(by:)
function is a (String, String) -> Bool
closure.
When we use type inference we call lessen the example code above into:
let fruits = ["Banana", "Coconut", "Apple"] // before – written in complete closure expression var sortedFruits = fruits.sorted(by: { (string1: String, string2: String) -> Bool in return string1 < string2 }) // after – using type inference var sortedFruits = fruits.sorted(by: { string1, string2 in return string1 < string2 })
In the example using type inference, both string1
and string2
no longer requires an explicit declaration of their type since the compiler infers both of them are String
types by looking at the sorted(by:)
function signature.
The example using type inference no longer declares the return type since it was also inferred by the compiler by looking at the sorted(by:)
function signature.
Implicit Returns from Single-Expression Closures
The return
keyword can be explicit when a closure only contains a single expression (or a one-line code).
// before – using type inference var sortedFruits = fruits.sorted(by: { string1, string2 in return string1 < string2 }) // after - using implicit returns var sortedFruits = fruits.sorted(by: { s1, s2 in s1 < s2})
Shorthand Argument Names
Inline closures as the one we’ve been using so far have shorthand argument names: $0
, $1
, and so on. The shorthand name refers to each closure parameter argument starting with $0
.
So for our example we used a (String, String) -> Bool
closure for the sorted(by:)
function, and we’ve name string1
and string2
or s1
and s2
as the argument names for the closure parameters from our previous examples.
Instead of using string1
or s1
, we can instead use the shorthand name $0
to refer to the first string parameter, and $1
to refer to the second string parameter that we named string2
, or s2
from the previous examples.
// before - using implicit returns var sortedFruits = fruits.sorted(by: { s1, s2 in s1 < s2 }) // after - shorthand argument names var sortedFruits = fruits.sorted(by: { $0 < S1 })
Trailing Closures
So far we’ve been passing inline clossures as parameters, but in Swift you can write a closure as a trailing closure
.
// before - shorthand argument names var sortedFruits = fruits.sorted(by: { $0 < $1 }) // after - trailing closures var sortedFruits = fruits.sorted { $0 < $1 }
A trailing closure
can only be written if it is the function’s last argument. Though, you can write multiple trailing closures as what you may have observed on some SwiftUI views, you’ll only need to write the argument label for the succeeding trailing closures
and not on the first one. An example to this would be a Button
. A Button
‘s init function accepts two functions as parameters: action : () -> Void
and label: () -> Label
.
// written in multiple trailing closures Button { print("Button tapped") } label: { Text("Tap me!") } // written in full closure expression syntax // w/ type inference Button( action: { in print("Button tapped") }, label: { in Text("Tap me!") } )
If you’ve been working on apps yourself, you may have worked on closures with your code. Closures are really helpful especially when used with functions.
We’ve still have so much more to explore using closures such as @escaping
closures, capturing values, and memory management, but these are discussions on best left on an article of their own.
That’s all for now! Cheers!
Got questions? Head over to our community forum where students and experts can help you!