Control Flow in Swift: `if`, `switch`, `guard`, and Pattern Matching


Every program needs to make decisions. Should Buzz Lightyear fly or fall? Does Remy get to cook, or does he hide? Without control flow, your code would run every line from top to bottom with no ability to choose a different path based on conditions.

In this guide, you’ll learn Swift’s three main decision-making tools: if/else, switch, and guard. We won’t cover loops or optionals here — those each have their own dedicated posts.

This guide assumes you’re familiar with operators and data types.

What You’ll Learn

What Is Control Flow?

Control flow is how your program decides which code to run based on conditions. Think of it like the sorting station at Monsters, Inc.: each door leads to a different room, and the system decides which door to open based on the current task.

Without control flow, every line of code runs in order, every time. With control flow, your program can take different paths depending on the data it’s working with.

if, else if, and else

The if statement is the simplest way to make a decision. If a condition is true, the code inside the braces runs.

let scarePower = 95

if scarePower > 90 {
    print("Top scarer!")
}
Top scarer!

Adding Alternatives

Use else to provide a fallback, and else if to check additional conditions:

let scarePower = 75

if scarePower > 90 {
    print("Top scarer!")
} else if scarePower > 50 {
    print("Solid performer")
} else {
    print("Needs training")
}
Solid performer

Swift evaluates conditions from top to bottom and runs the first block that matches. Once a match is found, it skips all remaining branches.

Combining Conditions

You can combine conditions using && (and) and || (or):

let age = 8
let hasTicket = true

if age >= 5 && hasTicket {
    print("Enjoy the Pixar movie marathon!")
}
Enjoy the Pixar movie marathon!

Tip: Keep conditions readable. If you have more than two or three combined conditions, consider extracting them into a descriptive variable: let canEnter = age >= 5 && hasTicket

switch Statements

When you’re checking one value against many possible cases, switch is cleaner than a chain of if/else if statements.

let movie = "Coco"

switch movie {
case "Toy Story":
    print("A story about toys coming to life")
case "Finding Nemo":
    print("A clownfish searches for his son")
case "Coco":
    print("A boy journeys to the Land of the Dead")
default:
    print("Another great Pixar film")
}
A boy journeys to the Land of the Dead

Exhaustive Matching

Swift requires every switch to be exhaustive — it must handle every possible value. For strings and numbers, this means you need a default case as a catch-all.

Note: This is a safety feature. Swift won’t let you accidentally forget a case. The compiler will tell you if your switch isn’t exhaustive.

Multiple Values in One Case

You can match several values in a single case using commas:

let rating = "PG"

switch rating {
case "G", "PG":
    print("Family friendly")
case "PG-13":
    print("Some material may be inappropriate")
case "R":
    print("Restricted")
default:
    print("Unknown rating")
}
Family friendly

Ranges in Switch

switch works beautifully with ranges:

let audienceScore = 87

switch audienceScore {
case 90...100:
    print("Certified Fresh — masterpiece!")
case 70..<90:
    print("Fresh — worth watching")
case 50..<70:
    print("Mixed reviews")
default:
    print("Rotten — skip it")
}
Fresh — worth watching

Note: Swift’s switch does not fall through to the next case by default. Unlike C or Java, you don’t need to write break at the end of each case. Each case automatically stops after its code runs.

Pattern Matching with switch

Swift’s switch goes far beyond simple equality checks. You can match tuples and use the where clause to add extra conditions.

Matching Tuples

let coordinates = (0, 10)

switch coordinates {
case (0, 0):
    print("At the origin")
case (_, 0):
    print("On the x-axis")
case (0, _):
    print("On the y-axis")
default:
    print("Somewhere else: \(coordinates)")
}
On the y-axis

The underscore _ acts as a wildcard — it matches any value. So (0, _) means “x is 0 and y can be anything.”

The where Clause

Add a where clause to filter cases with extra conditions:

let character = (name: "Buzz", age: 5)

switch character {
case let (name, age) where age < 10:
    print("\(name) is a young toy")
case let (name, _):
    print("\(name) is experienced")
}
Buzz is a young toy

The where clause lets you bind values and test them in one step. This becomes especially powerful when combined with enums and optionals.

Apple Docs: Control Flow — The Swift Programming Language

guard for Early Exits

The guard statement is like a bouncer at the door of Gusteau’s restaurant in Ratatouille. It checks a condition at the start and turns away anything that doesn’t qualify, so the rest of your code can proceed without worrying.

func printMovieReview(title: String, score: Int) {
    guard score >= 0 && score <= 100 else {
        print("Invalid score for \(title)")
        return
    }
    print("\(title) scored \(score)/100")
}

printMovieReview(title: "Inside Out", score: 95)
printMovieReview(title: "Bad Movie", score: -5)
Inside Out scored 95/100
Invalid score for Bad Movie

How guard Differs from if

The key difference: the else block in a guard statement must exit the current scope — using return, break, continue, or throw. This guarantees that after a guard passes, the condition is definitely true for all code that follows.

func greetCharacter(name: String?) {
    guard let characterName = name else {
        print("No character name provided")
        return
    }
    // characterName is guaranteed to exist here
    print("Hello, \(characterName)!")
}

greetCharacter(name: "Woody")
greetCharacter(name: nil)
Hello, Woody!
No character name provided

Tip: Use guard when you want to validate inputs early and avoid deeply nested if statements. It keeps your main logic at the top level of indentation.

Common Mistakes

Forgetting That switch Doesn’t Fall Through

// ❌ Expecting fall-through like C/Java
let genre = "Animation"
switch genre {
case "Animation":
    print("Animated film")
    // Does NOT fall through to the next case
case "Comedy":
    print("Funny film")
default:
    print("Other genre")
}
Animated film
// ✅ Use fallthrough explicitly if you need it
let genre = "Animation"
switch genre {
case "Animation":
    print("Animated film")
    fallthrough
case "Comedy":
    print("Funny film")
default:
    print("Other genre")
}
Animated film
Funny film

In Swift, switch cases don’t fall through by default. If you actually need fall-through behavior, use the fallthrough keyword — but this is rare in practice.

Non-Exhaustive switch Statements

// ❌ Won't compile — missing cases
let day = "Monday"
// switch day {
// case "Monday":
//     print("Start of the week")
// case "Friday":
//     print("Almost weekend")
// }  // Error: switch must be exhaustive
// ✅ Add a default case
let day = "Monday"
switch day {
case "Monday":
    print("Start of the week")
case "Friday":
    print("Almost weekend")
default:
    print("Regular day")
}
Start of the week

Every switch must cover all possible values. For open-ended types like String and Int, always include a default case.

What’s Next?

  • if/else handles simple branching decisions
  • switch matches values against multiple cases and must be exhaustive
  • Swift’s switch does not fall through by default — no break needed
  • guard validates conditions early and forces an exit if they fail
  • Pattern matching with tuples and where makes switch very powerful

Now that your code can make decisions, it needs to repeat actions. Head over to Loops in Swift to learn about for-in, while, and repeat-while.