Working With Guard in Swift

Guard Patterns, Tips, and Tricks

Working With Guard in Swift
1 Exit Early With Guard 08:17
2 Guard Patterns, Tips, and Tricks 06:27
Stop Writing Swift That Sucks

DISCLAIMER: No Rocket Science Involved

Join 20,000+ Developers Learning About Swift Development

Download the 4 Swift Patterns I Swear By

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.

Stop Writing Swift That Sucks

DISCLAIMER: No Rocket Science Involved

Join 20,000+ Developers Learning About Swift Development

Download the 4 Swift Patterns I Swear By