Conditionals are a fundamental part of programming in Swift.
The first conditional statement you learn is the if statement. It’s not the only one, though, nor is it the most used.
Another example is a guard statement. Guard statements are often much more common than if statements.
- This article is part of the Learn Swift series.
FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices
MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.
DOWNLOAD THE FREE GUIDETable of contents
- What is a guard statement, and how is it used in Swift?
- When to use a guard statement instead of an if statement
- What is the pyramid of doom problem, and how do guard statements solve it?
- Throwing errors when preconditions are not met
- Breaking out of a loop or skipping the current step
- Unwrapping optionals using a guard let statement
What is a guard statement, and how is it used in Swift?
In Swift, guard statements are used to transfer control out of a code block if one or more conditions are not met.
As mentioned above, they are conditional statements, just like an if statement. They are often used to check for invalid input or states that are not allowed in a particular part of a program.
Here is an example of a function calculating the area of a rectangle:
func calculateAreaOfRectangle(width: Double, height: Double) -> Double {
guard width > 0 && height > 0 else {
return 0
}
return width * height
}
The guard
statement checks that both the width
and height
arguments are greater than 0.
- If either of these conditions is not met, the code block inside the
guard
statement is executed. This returns 0. - If both conditions are met, control is transferred to the following line of code. This calculates and returns the area.
Note that a guard
statement must include an else
clause. This must transfer control to a code block that exits the current function or loop.
This is necessary because the guard
statement is designed to ensure that certain conditions are met. If they are not, the program should not continue.
It is also possible to transfer control by throwing an error, and we will look at how to do that shortly. We’ll also discuss using guard statements to break out of a loop, skip a step or unwrap optionals.
But first, let’s look at when to use a guard
statement and its advantages.
When to use a guard statement instead of an if statement
We could rewrite the code above to use an if
statement instead of a guard
statement.
func calculateAreaOfRectangle(width: Double, height: Double) -> Double {
if width > 0 && height > 0 {
return width * height
}
return 0
}
So, what is the difference, and why does the guard
statement exist?
Guard
statements were explicitly designed for early exits. This is their primary purpose.
One key benefit of using a guard
statement is that it allows you to handle invalid input or states at the beginning of a function. This prevents any need to check for these conditions throughout the function.
Guard
statements can also make your code more readable and easier to maintain, mainly when checking multiple preconditions.
Guard
statements also solve the pyramid of doom problem.
What is the pyramid of doom problem, and how do guard statements solve it?
We can best demonstrate the pyramid of doom problem with an example.
Let’s rewrite the function above to print an error whenever the width
or height
parameters are less than 0.
func calculateAreaOfRectangle(width: Double, height: Double) -> Double? {
if width > 0 {
if height > 0 {
return width * height
} else {
print("The height cannot be less than 0")
}
} else {
print("The width cannot be less than 0")
}
return nil
}
This is known as the pyramid of doom. Why? Because the indentation level increases as you go deeper into the code. This creates a pyramid-like shape.
This coding style can make it difficult to read and understand the logic of a program. This is especially true if there are many conditions or nested if
statements.
If you use a guard
statement to test the parameters and exit early, you can rewrite the above code as follows:
func calculateAreaOfRectangle(width: Double, height: Double) -> Double? {
guard width > 0 else {
print("The width cannot be less than 0")
return nil
}
guard height > 0 else {
print("The height cannot be less than 0")
return nil
}
return width * height
}
This code has the same effect as the original, but it is much easier to read and understand. All the conditions are checked at the top level, also known as the happy path, and the code is not nested.
Throwing errors when preconditions are not met
Returning a value is not the only way to transfer control out of a function. Guard
statements can also throw errors inside a throwing function.
For example, let’s rewrite the function above to throw an error instead of returning nil
when the area of a rectangle cannot be calculated.
enum RectangleError: Error {
case invalidWidth
case invalidHeight
}
func calculateAreaOfRectangle(width: Double, height: Double) throws -> Double {
guard width > 0 else {
throw RectangleError.invalidWidth
}
guard height > 0 else {
throw RectangleError.invalidHeight
}
return width * height
}
Breaking out of a loop or skipping the current step
Guard
statements are not only used to transfer control outside of a function. We can also use them inside loops to skip the current step or break out of the loop entirely.
The function below returns all the even numbers in an array until the first negative number.
func extractEvenNumbersUntilFirstNegative(from array: [Int]) -> [Int] {
var evenNumbers: [Int] = []
for number in array {
guard number > 0 else {
break
}
guard number % 2 == 0 else {
continue
}
evenNumbers.append(number)
}
return evenNumbers
}
let numbers = [1, 5, 2, 9, 4, 7, 6, -3, 8, 12]
extractEvenNumbersUntilFirstNegative(from: numbers)
// [2, 4, 6]
The function starts by declaring an empty array to store the even numbers. It then iterates over the input array using a for loop. For each number in the collection, it also performs the following steps:
- It checks whether the number is greater than 0. If it is not, the
break
statement causes the loop to exit immediately. - It checks whether the number is even. If it is not, the
continue
statement causes the loop to skip the current number and move on to the next one. - If the number is both positive and even, it is added to the
evenNumbers
array.
Unwrapping optionals using a guard let statement
We can also use guard
statements to unwrap optionals, by adding a let
or var
declaration.
For example, we can rewrite the function above to accept an array of strings instead of an array of integers.
func extractEvenNumbers(from array: [String]) -> [Int] {
var evenNumbers: [Int] = []
for string in array {
guard let number = Int(string) else {
continue
}
guard number % 2 == 0 else {
continue
}
evenNumbers.append(number)
}
return evenNumbers
}
let strings = ["1", "5", "nope", "4", "again", "6", "not", "8", "12"]
extractEvenNumbers(from: strings)
// [4, 6, 8, 12]
The function converts each string to an integer using the Int
initializer.
If the conversion fails, the guard let
statement causes the loop to skip over the current string. It might fail if the string is not a valid integer. The loop then moves on to the next one.
Conclusions
While guard
statements look very similar to if
statements, their purpose is quite different.
While if
statements are used for general control flow, guard
statements are used to check preconditions and exit a function or a loop as soon as possible.
In practice, the latter happens far more often than the former. So, while I still use if
statements from time to time, I almost exclusively use guard
statements.
SwiftUI App Architecture: Design Patterns and Best Practices
It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.
Matteo has been developing apps for iOS since 2008. He has been teaching iOS development best practices to hundreds of students since 2015 and he is the developer of Vulcan, a macOS app to generate SwiftUI code. Before that he was a freelance iOS developer for small and big clients, including TomTom, Squla, Siilo, and Layar. Matteo got a master’s degree in computer science and computational logic at the University of Turin. In his spare time he dances and teaches tango.