In the previous episode, we explored what the guard
statement is, how it differs from the if
statement, and when it's appropriate to use it. This episode takes it one step further. I show you several patterns I use on a daily basis that take advantage of the guard
statement.
Guard and Fatal Errors
Remember from the previous episode that a guard
statement always has an else
clause. The else
clause is required to transfer control to exit the scope in which the guard
statement is defined.
There are several ways you can transfer control. In the prepare(for:sender:)
method, we transferred control by returning from the method. Returning from a function or method is only one of the options you have to transfer control.
// 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
}
}
Take a look at this implementation of the tableView(_:cellForRowAt:)
method. Let me explain what's happening.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: NoteTableViewCell.reuseIdentifier, for: indexPath) as? NoteTableViewCell {
// Configure Cell
cell.textLabel?.text = "Some Setting"
return cell
} else {
return UITableViewCell()
}
}
We ask the table view for a table view cell with a specific reuse identifier. Because we're dealing with a UITableViewCell
subclass, we downcast the result of dequeueReusableCell(withIdentifier:for:)
to an instance of the NoteTableViewCell
class. We configure the table view cell if the downcast succeeds and we return a UITableViewCell
instance if the downcast fails.
This isn't ideal, though. We know we should expect a NoteTableViewCell
instance if we ask for a table view cell with that specific reuse identifier. The problem is that we're dealing with an optional.
We can add a pinch of elegance to this implementation by leveraging guard
and fatal errors. How do you feel about this implementation of the tableView(_:cellForRowAt:)
method?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue Reusable Cell
guard let cell = tableView.dequeueReusableCell(withIdentifier: NoteTableViewCell.reuseIdentifier, for: indexPath) as? NoteTableViewCell else {
fatalError("Unexpected Index Path")
}
// Configure Cell
configure(cell, at: indexPath)
return cell
}
Remember what I said about the guard
statement in the previous episode. The guard
statement is designed to exit early if one or more conditions are not met. We expect the table view to return an instance of the NoteTableViewCell
class if we ask it for one. If that operation fails, we decide to bail out by throwing a fatal error.
The fatalError(_:file:line:)
function doesn't return control, which means it can be used in the else
clause of the guard
statement. The result, however, is pretty dramatic. It causes the application to crash. While that may seem like a drastic action to take for a failed downcast, I feel it's an appropriate response. Why is that?
If the table view doesn't return a NoteTableViewCell
instance to the application, the latter doesn't know how to respond. The application enters a state it wasn't designed for. Some developers may argue that crashing the application isn't the right decision, but it has several benefits.
If this happens, it means you, the developer, made a mistake you need to fix. The application should never enter this state and you need to find out how the application ended up in this state.
Some developers may try to recover from unexpected states and that is a natural response, but terminating the application may be the safest option. This is a simple example. Imagine that you're dealing with a complex piece of logic that synchronizes the local database with a remote backend. If the application enters a state you didn't expect, it is possible the application is causing a lot of trouble. What if it entered a state in which it's deleting data? You don't know what's happening because you didn't anticipate this scenario. The solution is simple but drastic, terminating the application to mitigate risk and damage.
Guard and Pattern Matching
You may have heard of the if case
construct. It's also possible to use the case
keyword in combination with the guard
statement. Let me show you an example to clarify how this looks.
We define an enum, Voucher
, with three cases. Each case has one or more associated values.
enum Voucher {
case playStation(amount: Float)
case netflix(amount: Float, country: String)
case appStore(amount: Float, country: String)
}
We can create an instance of the Voucher
enum like this.
let voucher: Voucher = .netflix(amount: 25.00, country: "BE")
I'd like to implement a function that determines whether a voucher can be redeemed or not. The function, canRedeem(_:)
, accepts a Voucher
instance and returns a boolean.
func canRedeem(_ voucher: Voucher) -> Bool {
}
At the moment, the application only supports Netflix vouchers. We could solve this with an if
statement.
func canRedeem(_ voucher: Voucher) -> Bool {
if case voucher == .netflix {
// Redeem Voucher
// ...
return true
} else {
return false
}
}
But we can improve this by replacing the if
statement with a guard
statement. In this implementation, we bind the associated values of the Voucher
instance to local constants, amount
and country
.
func canRedeem(_ voucher: Voucher) -> Bool {
guard case let .netflix(amount, country) = voucher else {
return false
}
// Redeem Voucher
print(amount)
return true
}
We could take it one step further and only accept Netflix vouchers issued for the United Kingdom.
func canRedeem(_ voucher: Voucher) -> Bool {
guard case let .netflix(amount, country) = voucher, country == "UK" else {
return false
}
// Redeem Voucher
print(amount)
return true
}
This is how the canRedeem(_:)
function is used. Only the last invocation returns true
.
canRedeem(.playStation(amount: 100.0))
canRedeem(.netflix(amount: 50.00, country: "US"))
canRedeem(.netflix(amount: 100.00, country: "UK"))
It may take some time to wrap your head around this pattern so don't worry if it doesn't immediately click.
Elegance With Guard
I'd like to end this episode with a trick I learned from John Sundell, the developer behind Unbox and Marathon. This is something you've probably seen many times before.
private func setupNotificationHandling() {
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationDidEnterBackground, object: nil, queue: .main) { [weak self] notification in
// Save Data
self?.saveData()
}
}
// MARK: -
private func saveData() {
// Save Data
// ...
}
To avoid a retain cycle, we weakly capture self
by defining a capture list. This means self
is an optional in the closure we pass to the addObserver(forName:object:queue:using:)
method. Optional chaining is very convenient, but it isn't pretty to look at and I find that it complicates your code.
John Sundell uses a neat trick that uses the guard
statement. Take a look at the updated implementation. We safely unwrap self
and store it in a constant named self
. Because self
is a reserved word in Swift, we need to wrap the self
constant in backticks. The result looks a bit nicer.
private func setupNotificationHandling() {
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationDidEnterBackground, object: nil, queue: .main) { [weak self] notification in
guard let `self` = self else { return }
// Save Data
self.saveData()
}
}
// MARK: -
private func saveData() {
// Save Data
// ...
}
This is a legitimate use of the guard
statement. If self
is equal to nil
, it makes no sense to further execute the closure passed to the addObserver(forName:object:queue:using:)
method.
I must emphasize that this trick only works because of a bug in the compiler. It's likely this won't be possible in a future release of the Swift language.
If you don't like living on the edge, then replace the self
constant with _self
. That works just as well.
private func setupNotificationHandling() {
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationDidEnterBackground, object: nil, queue: .main) { [weak self] notification in
guard let _self = self else { return }
// Save Data
_self.saveData()
}
}
What's Next
The Swift language provides us with the tools and building blocks to create software. It's up to the developer to combine everything into something that works. But remember that code is more often read than written, which means that it pays to spend some time thinking about the code you write.