One of the key differences between junior and more senior developers is how they use the tools they are given. While a junior developer looks for any tool that gets the job done, a more senior developer looks for the tools that can get the job done and selects the most appropriate tool.
This very much applies to Swift's guard
statement. Developers new to the language don't quite understand why you would ever want to use a guard
statement. It looks confusing and it is similar to Swift's if
statement. There are other developers that use a guard
statement when an if
statement is more appropriate.
In this episode, I want to show you how and when to use the guard
statement. I also discuss when the if
statement is a more appropriate choice.
How Does Swift's Guard Statement Work
The first years of Swift development were a bit rough for many of us. Some of the best features were added in later releases of the Swift language. The guard
statement was one of those later additions.
The guard
statement is one of my favorite constructs of the Swift language and I hope this episode illustrates why that is. Let's start with an example. This is what a typical guard
statement looks like. I use this pattern time and time again in the prepare(for:sender:)
method.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else {
return
}
switch identifier {
case Segue.Note:
guard let destination = segue.destination as? NoteViewController else {
return
}
guard let indexPath = tableView.indexPathForSelectedRow else {
return
}
// Configure Destination
destination.note = notes[indexPath.row]
default:
break
}
}
The anatomy can be confusing if you are unfamiliar with the guard
statement. Let's focus on the first guard
statement of the prepare(for:sender:)
method.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else {
return
}
...
}
Unlike an if
statement, a guard
statement always has an else
clause. The else
clause of a guard
statement is executed when the expression of the guard
statement evaluates to false
. This also includes the scenario in which optional binding fails. This is something that trips up many developers that are new to the guard
statement.
When I am reading code, I replace the guard
keyword with the words "make sure that". Let's apply this to the first guard
statement of the prepare(for:sender:)
method. The guard
statement now reads likes this. Make sure that the optional binding of the identifier
property succeeds or else return from prepare(for:sender:)
method. That makes more sense. Right?
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else {
return
}
...
}
In this example, the else
clause is executed if the segue's identifier
property doesn't have a value. The body of the else
clause is simple. It contains a return
statement, which means it returns from the prepare(for:sender:)
method.
That is another important feature of the guard
statement. The else
clause is required to transfer control in such a way that it exits the scope in which the guard
statement is defined.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else {
return
}
...
}
If the expression of the guard
statement evaluates to true
or, in this example, if the optional binding of the identifier
property succeeds, code execution continues after the guard
statement and, more importantly, the variables and constants bound in the expression of the guard
statement are available after the guard
statement. That is why we can access the value of the identifier
constant in the switch
statement.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else {
return
}
switch identifier {
...
}
}
If you are still not grasping the meaning of the guard
statement, then let me replace the guard
statement with an if
statement.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
switch identifier {
...
}
}
}
This example illustrates that a guard
statement can be used to avoid, what is commonly referred to as, the pyramid of doom or multiple levels of indentation. That is something we can avoid by taking advantage of the guard
statement.
We can avoid nested if
and switch
statements by exiting as early as possible. That brings us to the true purpose of the guard
statement.
Keep It Simple
Despite your best efforts, some functions and methods are complex. You need to use a bunch of if
and switch
statements, which can become messy very quickly. It also makes testing a pain.
To keep the implementation of complex functions and methods simple, it helps to exit as early as possible. This is what that looks like in Objective-C.
- (NSArray *)fetchNotes:(NSArray *)notes {
if (!self.isReachable) return nil;
if (!self.isConnected) return nil;
if (!notes || !notes.count) return nil;
// Fetch Notes
// ...
return @[];
}
This is the only use case in which I omit the curly braces of an if
statement in Objective-C. It improves readability and it doesn't pose any risk as long as you keep the if
statement on one line.
This pattern is a good start to simplify the implementation of the fetchNotes:
method because it reduces the need for nested if
statements. This is what the implementation would look like if we were to nest the if
statements.
- (NSArray *)fetchNotes:(NSArray *)notes {
if (self.isReachable && self.isConnected) {
if (notes && notes.count) {
// Fetch Notes
// ...
return @[];
}
}
return nil;
}
The implementation in Swift looks similar. It is a bit cleaner because we don't need to use the self
keyword. But notice that we are stuck with at least one level of nesting because we need to safely unwrap the value stored in the notes
parameter.
private func fetch(notes: [String]?) -> [Note]? {
if !isReachable { return nil }
if !isConnected { return nil }
if let notes = notes, notes.count > 0 {
return []
}
return nil
}
Exit Early With Guard
The Swift team added the guard
statement in Swift 2 to resolve these issues. The guard
statement isn't very different from a regular if
statement. The main difference is that the guard
statement is designed to exit early from the current scope if the expression of the guard
statement evaluates to false
.
Let's revisit the implementation of the prepare(for:sender:)
method I showed you earlier. Because we access the value of the segue's identifier
property in the switch
statement, we are only interested in segues that have an identifier. If a segue doesn't have an identifier, we can return immediately from the prepare(for:sender:)
method.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else {
return
}
switch identifier {
case Segue.Note:
guard let destination = segue.destination as? NoteViewController else {
return
}
guard let indexPath = tableView.indexPathForSelectedRow else {
return
}
// Configure Destination
destination.note = notes[indexPath.row]
default:
break
}
}
The same applies to the switch
statement. In the body of the first case, we downcast the destination view controller to an instance of the NoteViewController
class. If that fails, it makes no sense to execute the remaining code of the body of the case. And the same is true for the third guard
statement of the prepare(for:sender:)
method.
Designed For Elegance and Convenience
I mentioned earlier that the guard
statement is similar to the if
statement, but there is an important difference. What makes the guard
statement powerful is that the values that are safely unwrapped through optional binding are available in the scope in which the guard
statement is defined. That feature allows us to use the value stored in the identifier
constant in the switch
statement.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else {
return
}
switch identifier {
...
}
}
Let's refactor the implementation of the fetch(notes:)
method by replacing the if
statements with guard
statements. Remember that the expression of the guard
statement needs to evaluate to true
to avoid entering the else
clause of the guard
statement. This is what we end up with. Notice that the expression of the guard
statement can contain multiple conditions, separated by a comma.
private func fetch(notes: [String]?) -> [Note]? {
guard isReachable else { return nil }
guard isConnected else { return nil }
guard let notes = notes, !notes.isEmpty else { return [] }
// Fetch Notes
// ...
return nil
}
Guard or If
Even though the guard
statement looks very appealing, it shouldn't be your first or default choice. The if
statement should be your default choice. Use the guard
statement only if you guard against something. In other words, if a condition must be met for the rest of the code to make sense, then the guard
statement is the best choice. Most of the time, you will find yourself using an if
statement.
What I often recommend is to start with an if
statement and, once everything is working as expected, refactor the implementation by adding one or more guard
statements if that makes sense.
From the moment you are using nested if
statements it may be time to break the function or method up into several smaller functions or methods, or, to introduce one or more guard
statements.
Take a look at this example. If the table view asks us for the height of the first row of a section, we return 60.0
, otherwise we return 44.0
.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
return 60.0
} else {
return 44.0
}
}
We could rewrite this implementation by using a guard
statement instead. The result is identical, but this is not how the guard
statement should be used. It only complicates the implementation. While I am sure some developers would argue that it is fine to use a guard
statement, this is not what it was designed for.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard indexPath.row > 0 else {
return 66.0
}
return 44.0
}
If you want to make the implementation of the tableView(_:heightForRowAt:)
method a bit more elegant, then remove the else
clause and put the if
statement on one line. There's no need for a guard
statement in this example.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 { return 66.0 }
return 44.0
}
I have also seen some developers do the following. It looks odd at first, but it is very easy to read.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 { return 66.0 }
else { return 44.0 }
}
Patterns, Tips, and Tricks
In the next episode, I share with you several patterns and tricks that take advantage of the guard
statement.