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.
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 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.