Delve into the world of iOS programming with a clear focus on understanding Swift closures, a fundamental yet often misunderstood feature of Swift. This guide is tailored to demystify closures, presenting them as not just a coding construct, but a pivotal tool in your Swift programming arsenal. From the basics of their structure to the intricacies of their use, we will explore how understanding Swift closures can unlock new possibilities in your code, making it more efficient, readable, and powerful. Whether you’re a beginner or an experienced developer, this comprehensive look into Swift closures is designed to enhance your coding proficiency in this versatile language.
Btw, if you are not a Swift developer, we recommend you check out my other articles on Swift here.
Let’s jump right in.
What are Closures?
The concept of closures might seem deceptively complex at first glance, especially when exposed to them without much context or a solid grasp of object-oriented programming patterns. However, in reality, the idea behind them is pretty simple and easy to grasp.
As Apple has stated: “Closures are self-contained blocks of functionality that can be passed around and used in your code.” This definition might sound a bit simplistic and abstract, but it’s pretty on point.
A closure is simply a self-contained block of code or “functionality” that behaves like a variable, allowing you to reference it anywhere like any other variable. In short, a closure is a function without the name associated with it.
This definition becomes more apparent once you actually see a closure in code. For example, here’s a simple closure that prints the titular “hello world” phrase in Swift.
{ (param: type) -> returnType in
print("Hello World!")
}
As you can see, it basically comes down to being the body of a function.
Notice, however, that the structure is a tidbit different from a basic swift function. The parameters and returning type lie after the opening bracket and are suffixed by the optional keyword “in.” These are pretty self-explanatory.
Additionally, you could due away with the parameters and returning type directives and simply write the function as follows.
{
print("Hello World!")
}
Simple and clean.
Using Closures
Now, how can we use these closures in a real scenario?
Well, in many ways, really.
You can simply assign the closure to a variable and invoke it just like any function.
var greetMe = { (name: String, age: Int) in
print("Hi \(name), you are \(age) years old!")
}
greetMe("Juan", 30)
Notice that there are two parameters provided and no returning type since the closure function does not require to return anything.
If you were to run this code in a playground, you would be greeted with the following text.
"Hi Juan, you are 30 years old!"
OK, but why is this different than a function? Clearly, you could create a function with the same amount of lines of code and have the same result, right?
Well, yes. But that is only because we are yet to explore the real flexibility that comes intrinsically in closures and is not available for a function. More on that later.
Why Do We Need Closures in Swift?
This is a tricky question to answer because, thanks to the flexibility and robustness of the Swift language and its syntax, you could be a proficient and capable developer and never need to use a closure. But this speaks more of the versatility of the language and not so much about the uselessness of the closure as a tool.
In essence, developing with closures mainly awards you a more readable and maintainable codebase. It also allows you to make your code more DRY and versatile by encapsulating complexity and making it reusable. Finally, Swift has a particular use-case for closures that makes the code more compact and streamlined, and that is the trailing closures on function parameters.
Here’s an example of a function that requires three parameters, a string, a number, and a closure. Notice how the syntax of the function call is expressed.
func isOldEnough(name: String, age: Int, onOldEnough: (String) -> (String)) {
print("Checking if \(name) is old enough...")
if age > 18 {
print(onOldEnough(name))
} else {
print("\(name) is not old enough yet!")
}
}
isOldEnough(name: "Juan", age: 30) { name in
return "Congrats \(name), you are old enough!"
}
Now, what is going on here?
- First, the function stays pretty consistent with the standard syntax, except that the last parameter indicates the closure syntax on its type directive.
- Then, as you can see, the closure is invoked only when the specified condition is met.
- Finally, the function call is below, but a closure now occupies where the last parameter would reside.
This closure merely contains a few instructions that the function will execute according to its conditions.
How About Functions?
Now, could this have been done with a function?
Yes, absolutely.
func whenOldEnough(name: String) -> String {
return "Congrats \(name), you are old enough!"
}
func isOldEnough(name: String, age: Int, onOldEnough: (String) -> (String)) {
print("Checking if \(name) is old enough...")
if age > 18 {
print(onOldEnough(name))
} else {
print("\(name) is not old enough yet!")
}
}
isOldEnough(name: "Juan", age: 30, onOldEnough: whenOldEnough)
But the former pattern is more compact, concise, and arguably, more readable, which is the objective of closures.
What is the Difference Between a Closure and a Function?
After seeing the examples above, it’s hard not to argue that the difference between closures and functions is pretty insignificant. And guess what, you would be right. But that doesn’t mean that they don’t behave differently.
A global function, which is the basic function syntax you have seen, does not capture values. And by capturing values, I mean keeping a reference to objects referenced inside it that lie outside its scope. However, a closure does.
Let’s illustrate.
A Simple Example
If you were to create a function like the following, which contains a closure inside of it, executes some operations, and returns a reference of said closure, you would get the following behavior.
func getAgeOf(name: String) -> (() -> ()) {
var age: Int = 10
let addAge = {
age += 1
print("The age of \(name) is now \(age)")
}
return addAge
}
var test = getAgeOf(name: "Juan")
test() // "The age of Juan is now 11"
test() // "The age of Juan is now 12"
test() // "The age of Juan is now 13"
test() // "The age of Juan is now 14"
test() // "The age of Juan is now 15"
Now, I know that this seems like a complex and convoluted example, but what is happening here is quite simple.
Understanding Swift Closures
The ‘getAgeOf
‘ function initializes two variables, an Int for the age, and a closure, which it then returns. The closure contains a reference of the age variable, which is initialized as “10” and increments its value by one. It then proceeds to print the current value of the age variable with a message.
What we are doing here is simply “constructing” a closure with some predefined values, which are kept by reference. And so, when you call the ‘getAgeOf
‘ function, you get a closure that you can call and invoke as many times as you want.
Now, since the value of the age variable is referenced inside the closure, it is kept in memory and “remembered” for as long as the closure itself is kept in memory. This means that every time we call the closure, it increments the value accordingly.
If you were to try and reproduce this pattern with a global function, it wouldn’t work. This is because global functions don’t have a way to keep the values of variables outside of their scope.
However, you could reproduce the closure behavior with nested functions, which we essentially call closures.
So, in short:
- Global functions have a name and don’t capture values.
- Nested functions have a name and capture referenced values outside of their scope.
- Closures don’t have a name and capture referenced values outside of their scope.
When Would You Use Closures Instead of Functions?
The argument of where to use closures instead of function comes to preferences. This is mainly due to the versatility and robustness of the Swift language.
Given that, for the most part, there’s very little incentive outside of strict development patterns and guidelines on teams and organizations, you could argue that the only argument is, what are you more comfortable with or more productive.
For the most part, the community seems to agree that closures are practical tools for reducing the apparent complexity of code and making life easier in the long run when one needs to maintain projects.
Moreover, it seems pretty clear that closures allowing the capture of values outside of their scope might incentivize more bugs, given that lazy or inexperienced programmers might not be able to handle references properly. And this is a perfectly valid point. However, this issue is largely mitigated by the platform’s ability to manage memory issues.
So, in our opinion, as long as you know what you are doing by referencing a variable that’s initialized outside of the closure, you should be fine.
What’s Next?
Like all other programming patterns available in Swift, Closures are just a tool. And like all tools, they are as powerful or as dangerous as the capacity of the user.
Thankfully, the Swift programming language has come a long way from Obj-c blocks and the nightmare of memory leaks.
If you want to ensure that your project is bug-free and ready for primetime, you need to have a solid testing workflow.
Sometimes, however, building this workflow can be expensive and time-consuming, especially for lean and specialized teams.
Check out my other articles on the topic here.
This post was originally written for and published by Waldo.com
Leave a Reply