Control flow is an essential aspect of any programming language. In this and the next episode, we discuss loops and conditionals. We start with conditionals.

Conditionals are used to determine which code needs to be executed when. Swift defines three constructs.

  • if
  • guard
  • switch

Let's start with the construct you're probably already familiar with, the if construct.

If

We've already encountered a few if statements in this series. The if statement is easy to understand and requires little explanation. In this example, we execute the showLoginView() function if the value of isUserLoggedIn is equal to false.

let isUserLoggedIn = true

if !isUserLoggedIn {
    showLoginView()
}

Unlike C and Objective-C, there's no need to wrap the condition in parentheses. The body of the if clause, however, should always be wrapped in a pair of curly braces. That's a good thing because it avoids common bugs and promotes consistency.

Curly braces are required for if statements in Swift.

Swift's if statement also supports an else clause and chaining of if statements. If the value of isUserLoggedIn is equal to true, we check if the user has started a trial membership. If they have, we show them a list of payment plans. If they already have a paid membership, we take them to their dashboard.

let isUserLoggedIn = true
let hasTrialMembership = true

if !isUserLoggedIn {
    showLoginView()
} else if hasTrialMembership {
    showPaymentPlans()
} else {
    showDashboard()
}

Guard

Even though the if statement is great and indispensable in every application, complex application logic often leads to nested if statements. While nesting isn't a problem in and of itself, it can result in code that's difficult to read and understand. Even more so when if statements are used in combination with optional binding.

This example is a bit contrived, but it shows the problem of nesting multiple if statements. The fetchSales() function fetches the sales for a user. It returns false if it cannot perform the API request.

import Foundation

func fetchSales() -> Bool {
    if User.isLoggedIn {
        if let user = User.current {
            if var url = URL(string: API.users) {
                url = url.appendingPathComponent("\(user.id)")

                URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
                    // Handle Response
                    // ...
                })

                return true
            }
        }
    }

    return false
}

Before we can send the request to the API, we need to make sure we have access to the current user and that the user is logged in. We need to plough through three if statements before we can make the API request.

We can greatly simplify the fetchSales() function by replacing the if statements with guard statements. This is what the implementation of the fetchSales() function looks like with guard statements.

import Foundation

func fetchSales() -> Bool {
    guard User.isLoggedIn else {
        return false
    }

    guard let user = User.current else {
        return false
    }

    guard var url = URL(string: API.users) else {
        return false
    }

    url = url.appendingPathComponent("\(user.id)")

    URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
        // Handle Response
        // ...
    })

    return true
}

A guard statement is similar to an if statement. The key differences are that a guard statement always has an else clause and that variables and constants assigned using optional binding are available in the scope the guard statement was defined in. That's why the url variable is available to us after the third guard statement.

Another important difference with Swift's if statement is that the else clause of the guard statement needs to transfer control. What does that mean? The guard statement is designed for one purpose, exiting early. The refactored implementation of the fetchSales() function illustrates this. The application returns from the function as soon as it knows the request cannot be sent to the API.

It does this by transferring control in the else clause, exiting the function. If we don't transfer control in the else clause, the compiler throws an error. The compiler warns us that we need to exit the scope in which the guard statement is defined, for example, by returning from the function.

The body of the else clause of a guard statement cannot fall through.

The guard statement is a wonderful addition to the Swift language. It improves code clarity and readability by keeping nested if statements to a minimum.

Switch

Basics

Swift's switch statement is incredibly powerful and versatile, and that's not an exaggeration. The basic syntax looks like this.

let statusCode = 404
var statusCodeAsString = "OK"

switch statusCode {
case 200:
    statusCodeAsString = "OK"
case 201:
    statusCodeAsString = "Created"
case 202:
    statusCodeAsString = "Accepted"
case 400:
    statusCodeAsString = "Bad Request"
case 401:
    statusCodeAsString = "Unauthorized"
case 403:
    statusCodeAsString = "Forbidden"
case 404:
    statusCodeAsString = "Not Found"
default:
    break
}

In this example, we switch on the value stored in statusCode. We start the switch statement with the switch keyword, followed by the variable or constant whose value is evaluated by the switch statement. The cases of the switch statement are wrapped in a pair of curly braces.

Swift's switch statement behaves quite differently from that in other programming languages. In Swift, switch statements need to be exhaustive. This means that every possible value of the type that's being evaluated needs to have a corresponding case in the switch statement. Because it isn't possible to add a case for every possible value of an Int, the type of statusCode, we add a default case. The body of the default case is executed if no other match was found.

This brings us to another difference. The body of every case needs to have at least one statement. For that reason we add a break statement to the default case. The body of the default case cannot be empty.

Fallthrough

If you're coming from C or Objective-C, then you may be wondering why we don't add a break statement to each case. Remember that Swift is obsessed with safety. For switch statements, that means there's no implicit fallthrough. As soon as a match is found, the switch statement is exited.

While this may be safe, it leads to code duplication. What do you do if several cases need to execute the same code? Don't worry. Swift has you covered. In this example, we switch on the value of name. Notice that the first case defines three patterns instead of one. The patterns are separated by commas.

let name = "Bart"

switch name {
case "Bart", "Lucy", "Jim":
    print("You deserve a break.")
default:
    print("I'm sorry. You need to work a little more.")
}

This switch statement is equivalent to this switch statement. We place each pattern on a separate line to improve readability.

let name = "Bart"

switch name {
case "Bart",
     "Lucy",
     "Jim":
    print("You deserve a break.")
default:
    print("I'm sorry. You need to work a little more.")
}

Flexibility

The switch statement in C and Objective-C is quite limited in terms of what you can do with it. In Swift, the type you switch on isn't limited to integers. In this example, we switch on a string.

let name = "Bart"

switch name {
case "Bart":
    print("Hello, Bart")
case "Lucy":
    print("Hello, Lucy")
case "Marc":
    print("Hello, Marc")
case "John":
    print("Hello, John")
case "Katie":
    print("Hello, Katie")
default:
    print("Hello")
}

But it doesn't stop here. Swift's switch statement also supports pattern and interval matching. In this example, we use the half-open range operator to evaluate the status code of a network request.

var success = false
let statusCode = 203

switch statusCode {
case 200..<400:
    success = true
default:
    break
}

Tuples, Where, and Value Binding

Did I mention that Swift's switch statement also supports tuples? The syntax is what you'd expect. We define a tuple, employee, with three values. We evaluate the values stored in employee using a switch statement. The first case is a match if the value of seniority is greater than 5 and less than or equal to 100.

var needsPromotion = false
let employee = (name: "Lucy", seniority: 6, skills: ["iOS", "Backend", "Project Management"])

switch employee {
case (_, 5...100, _):
    needsPromotion = true
case let (_, _, skills) where skills.count > 2:
    needsPromotion = true
    print("This employee has \(skills.count) skills and qualifies for promotion.")
default:
    break
}

Are you confused by the underscores in the example? The underscore character indicates that any value is a match. That's why the underscore character is also referred to as the wildcard character.

The example also illustrates the use of the where keyword to define additional conditions for a case. The second case is a match if the employee has more than two skills listed in her profile.

The second case also shows us how value binding works in a switch statement. We bind the value of the employee's skills in a temporary constant, skills. We can use the skills constant in the where clause and it's also available in the body of the case. This is very convenient, especially when working with enumerations. We discuss enumerations later in Swift Fundamentals.

What's Next?

I'm sure you agree that the if and guard statements are easy to use and understand. The switch statement may be a bit more daunting due to its amazing flexibility and versatility. In the next episode, we continue exploring control flow in Swift by taking a look at loops.