Closures
Just like functions, closures enable us to define a group of code statements that can be called as one unit, which can both accept input and produce output. The difference, however, is that closures can be defined inline — right at the point where we want to use them — which is incredibly useful in a number of different situations.
Closures can both be stored as properties and local variables, and can also be passed as arguments to functions (or other closures), like this:
struct IntToStringConverter {
// A closure property that takes an Int as input
// and produces a String as output:
var body: (Int) -> String
}
// A closure defined as an inline variable, which
// takes no input and produces an Int as output:
let intProvider: () -> Int = { return 7 }
// A closure function argument that takes no input
// and also doesn't produce any output:
func performOperation(then closure: () -> Void) {
...
}
To take a look at how closures can be really useful in practice, let’s say that we wanted to extend Swift’s String
type with a function that enables us to transform each word that appears within a string. By using a closure, we can enable the callers of our new function to freely decide exactly how to perform each transform:
extension String {
func transformWords(
using closure: (Substring) -> String
) -> String {
// Split the current string up into word substrings:
let words = split(separator: " ")
var results = [String]()
// Iterate through each word and transform it:
for word in words {
// We can call the closure that was passed into our
// function just like how we'd call a function:
let transformed = closure(word)
results.append(transformed)
}
// Join our results array back into a string:
return results.joined(separator: " ")
}
}
Note that the above sample code is just an example, as it’s not a very efficient way to transform strings. To learn more about strings and how the above Substring
type works, check out the “Strings” article.
We can now call the above function with any closure that we want, as long as it takes a Substring
as input, and produces a String
as output. For example, here’s how we could transform each word within a string into lowercase:
let string = "Hello, world!".transformWords(using: { word in
return word.lowercased()
})
print(string) // "hello, world!"
The in
keyword that we use above is, among other things, used to name the arguments that our closure accepts.
Compared to most other language features, Swift’s closure syntax is quite flexible, and there’s a number of features that we can take advantage of in order to make the above code a bit more compact:
- Using trailing closure syntax, we can simply append our closure to the name of the function that we’re calling, rather than having to type out the parentheses and the parameter label.
- We can replace
word in
with the$0
closure argument shorthand, which lets us refer to the first (and in our case, only) argument passed into the closure. - We can remove the
return
keyword, which is not required for single-expression closures (a feature that, starting with Swift 5.1, also applies to functions and computed properties).
By applying all of the above, our code will now look like this instead:
let string = "Hello, world!".transformWords { $0.lowercased() }
When using a more compact syntax, we always have to be careful not to reduce the readability of our code too much — so for more complex closures, using a more verbose syntax variant may be more appropriate.
Not only can newly defined closures be passed as function arguments, we can also pass an existing closure on to another function as well. For example, here’s how we could go back to the implementation of our transformWords
function, and simply pass the given closure to map
— rather than having write a manual iteration:
extension String {
func transformWords(
using closure: (Substring) -> String
) -> String {
let words = split(separator: " ")
let results = words.map(closure)
return results.joined(separator: " ")
}
}
To learn more about map
, check out the “Map, FlatMap and CompactMap” article.
So far, we’ve only used closures that are immediately executed and then discarded — but it’s also very common to want to store a closure for later use. For example, let’s say that we wanted to write a delay
function, that lets us delay the execution of any closure by a certain amount of seconds. To do that, we’ll use Grand Central Dispatch’s asyncAfter
API — however, since passing our closure to that API will cause it to be stored until our delay time interval has passed, we’ll need to mark it as @escaping
, like this:
func delay(by seconds: TimeInterval,
on queue: DispatchQueue = .main,
closure: @escaping () -> Void) {
queue.asyncAfter(
deadline: .now() + seconds,
execute: closure
)
}
When a closure is marked as @escaping
, it both means that it can be stored for later use, and it also means that we’ll need to explicitly use self
whenever we’re accessing an instance property or method within it:
class ProfileViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
delay(by: 2) {
// We need to use 'self' here to be able to call our
// method, since we're inside an escaping closure.
self.showTutorialIfNeeded()
}
}
private func showTutorialIfNeeded() {
...
}
}
However, there’s something that we need to be very careful with when writing code like the above — and that’s capturing. The reason we have to explicitly use self
within escaping closures is that it causes that object to be captured, meaning that it’ll be retained in memory for as long as the closure itself remains in memory, which can cause memory leaks if we’re not careful.
Like we took a look at in the “Memory Management” article, one way to prevent a strong capture from happening is to use a capture list to specify that we’d like to capture self
weakly — which won’t cause it to be strongly retained:
delay(by: 2) { [weak self] in
self?.showTutorialIfNeeded()
}
Note that in the above example, it’s not really a big deal that we capture self
strongly, as our closure will only be retained for 2 seconds. However, as we might increase that time interval in the future, it can still be a good idea to use weak
— especially since there’s no reason for self
to be retained strongly in this case anyway.
Finally, let’s take a look at how the line between functions and closures gets even more blurred as we start utilizing Swift’s first class function capabilities. Going back to our earlier example of transforming words, let’s say that we’ve defined a standard function that capitalizes any word passed to it:
func capitalize(word: Substring) -> String {
return word.capitalized
}
Since Swift supports first class functions, we can actually pass the above function just as if it was a (Substring) -> String
closure — which in turn lets us use it as an argument when calling our previous transformWords
function, since it accepts a closure with that exact shape:
let name = "swift by sundell".transformWords(using: capitalize)
print(name) // "Swift By Sundell"
Pretty cool! There’s of course a huge number of other things that we can do with closures in Swift, some of which can take us deeper into the realm of functional programming — but I hope that this article has given you a quick basic overview of how closures can be used in Swift.
Thanks for reading! 🚀