Today we’ll look at control flow statements like if
, for
, and switch
. These statements are the decision makers of Swift, allowing you to create dynamic software. Understanding control flow means you can write code that can branch, loop, and make decisions based on the data it encounters.
What is Control Flow?
Control flow determines the order in which your code executes. Instead of just running line by line from top to bottom, control flow lets you skip sections, repeat operations, or choose between different paths based on conditions. It’s what transforms a simple list of instructions into intelligent software.
Swift provides several powerful control flow tools that work seamlessly with the operators and data types you’ve already learned.
Conditional Statements: Making Decisions
Conditional statements let your code make decisions. They’re like the crossroads in your program where you choose which path to take.
The if
Statement
The most basic decision-maker is the if
statement. It runs code only when a condition is true.
let playerLevel = 25
if playerLevel >= 20 {
print("Battle pass unlocked!")
}
if-else
: Two Paths
When you need to handle both true and false cases, use if-else
:
let hasKeyCard = true
if hasKeyCard {
print("Enter the bunker!")
} else {
print("Access denied, requires key card.")
}
if-else if-else
: Multiple Conditions
For more complex decision trees, chain multiple conditions:
let heroScore = 85
if heroScore >= 90 {
print("Welcome to the Avengers! 🦸♂️")
} else if heroScore >= 80 {
print("You're a S.H.I.E.L.D. agent!")
} else if heroScore >= 70 {
print("You're a friendly neighborhood hero.")
} else {
print("Keep practicing, rookie!")
}
Typical Use Cases:
- User authentication and authorisation
- Form validation and input checking
- Feature flags and conditional functionality
- Game state management (win/lose conditions)
Guard Statements: Early Exit
The guard
statement is Swift’s way of handling early exits gracefully. It’s perfect for validation and preventing running code in invalid states.
Basic Guard:
func processUser(name: String?) {
guard let userName = name else {
print("Error: No name provided")
return
}
print("Processing user: \(userName)")
// Continue with the rest of the function
}
Multiple Conditions:
func authenticateUser(username: String?, password: String?) {
guard let user = username,
let pass = password,
!user.isEmpty,
!pass.isEmpty else {
print("Authentication failed: Missing credentials")
return
}
print("Authenticating \(user)...")
// Authentication logic here
}
Reusing the Variable Name with guard let name else
If you want to reuse the same variable name after unwrapping, Swift lets you write:
guard let name else {
print("No name provided")
return
}
print("Hello, \(name)!")
This is handy when you want to keep your code concise and avoid introducing a new variable just for the unwrapped value.
Typical Use Cases:
- Input validation at the start of functions
- Unwrapping optionals safely
- Early returns when preconditions aren’t met
- API parameter validation
Switch Statements: Pattern Matching
Swift’s switch
statements are incredibly powerful. They perform pattern matching on data.
Basic Switch:
let weatherCondition = "sunny"
switch weatherCondition {
case "sunny":
print("Perfect day for a picnic! ☀️")
case "rainy":
print("Better bring an umbrella ☔")
case "snowy":
print("Time for hot chocolate ❄️")
default:
print("Weather is unpredictable today")
}
Multiple Values:
let characterClass = "mage"
switch characterClass {
case "warrior", "paladin", "knight":
print("You specialise in melee combat")
case "mage", "wizard", "sorcerer":
print("You master the arcane arts")
case "rogue", "assassin", "thief":
print("You excel at stealth and cunning")
default:
print("Your class is unique and mysterious")
}
Range Matching:
let experience = 1250
switch experience {
case 0..<100:
print("Rookie adventurer")
case 100..<500:
print("Seasoned explorer")
case 500..<1000:
print("Veteran warrior")
case 1000...:
print("Legendary hero")
default:
print("Experience level unknown")
}
Typical Use Cases:
- State machine implementations
- Processing different types of user input
- Categorising data into groups
- Handling various error types
Loops: Repeating Operations
Loops let you repeat operations efficiently, whether you’re processing collections or repeating actions a certain number of times.
For-In Loops: Iterating Collections
The most common loop iterates over collections:
let spells = ["Fireball", "Ice Storm", "Lightning Bolt", "Heal"]
for spell in spells {
print("Casting \(spell)!")
}
Range-Based Loops:
// Print countdown
for countdown in (1...5).reversed() {
print("\(countdown)...")
}
print("Launch! 🚀")
// Process items with indices
let inventory = ["sword", "potion", "scroll"]
for index in 0..<inventory.count {
print("Slot \(index): \(inventory[index])")
}
While Loops: Condition-Based Repetition
Use while
when you don’t know exactly how many iterations you need:
var lives = 3
var gameOver = false
while lives > 0 && !gameOver {
print("You have \(lives) lives remaining")
// Game logic here
lives -= 1
if lives == 1 {
print("Last chance!")
}
}
Repeat-While: Execute At Least Once
When you need to run the loop body at least once:
var playerInput: String
repeat {
print("Enter a command (type 'quit' to exit):")
playerInput = readLine() ?? ""
print("You entered: \(playerInput)")
} while playerInput != "quit"
Typical Use Cases:
- Processing arrays and collections
- Reading user input until valid
- Game loops and animation frames
- Batch operations on data
Loop Control: Breaking and Continuing
Sometimes you need fine control over loop execution.
Break: Exit the Loop
The break
statement lets you break out of the loop entirely.
let treasureLocation = 7
for room in 1...10 {
print("Searching room \(room)...")
if room == treasureLocation {
print("Treasure found! 💰")
break // Exit the loop immediately
}
}
Continue: Skip to Next Iteration
The continue
statement lets you skip the rest of the current iteration but the loop will continue from the next iteration.
let forbiddenRooms = [3, 7, 9]
for room in 1...10 {
if forbiddenRooms.contains(room) {
print("Room \(room) is locked. Skipping...")
continue // Skip the rest of this iteration
}
print("Exploring room \(room)")
// Room exploration logic here
}
Labeled Statements: Control Nested Loops
For complex nested loops, you can use labels. In the following code we label the top level for loop as gameLoop
, then specify that we want to break out of the gameLoop
, ultimately breaking out of both loops.
gameLoop: for level in 1...3 {
print("Starting level \(level)")
for enemy in 1...5 {
print("Fighting enemy \(enemy)")
if enemy == 3 && level == 2 {
print("Boss appeared! Ending game.")
break gameLoop // Breaks out of both loops
}
}
}
Typical Use Cases:
- Early termination when a condition is met
- Skipping invalid or unwanted data
- Complex nested loop control
- Search algorithms (finding the first match)
Combining Control Flow
Real-world code often combines multiple control flow techniques:
func processPlayerActions(_ actions: [String]) {
guard !actions.isEmpty else {
print("No actions to process")
return
}
for (index, action) in actions.enumerated() {
print("Action \(index + 1):")
switch action.lowercased() {
case "attack":
print("⚔️ Player attacks!")
case "defend":
print("🛡 Player defends!")
case "heal":
print("❤️ Player heals!")
case "run":
print("💨 Player runs away!")
break // Exit the loop
default:
print("❓ Unknown action: \(action)")
continue // Skip to next action
}
// Simulate some processing time
if index < actions.count - 1 {
print("Preparing next action...\n")
}
}
print("All actions processed!")
}
// Usage
let playerActions = ["Attack", "Heal", "unknown", "Defend", "Run", "Attack"]
processPlayerActions(playerActions)
Best Practices
1. Prefer Guard for Early Returns:
// Good
func validateInput(_ text: String?) -> Bool {
guard let input = text, !input.isEmpty else {
return false
}
return input.count >= 3
}
// Less ideal
func validateInputNested(_ text: String?) -> Bool {
if let input = text {
if !input.isEmpty {
return input.count >= 3
}
}
return false
}
2. Use Switch for Multiple Conditions:
// Good
switch controllerType {
case "DualSense":
configureDualSense()
case "DualShock":
configureDualShock()
case "ThirdParty":
configureThirdPartyController()
default:
showControllerError()
}
// Less clean
if controllerType == "DualSense" {
configureDualSense()
} else if controllerType == "DualShock" {
configureDualShock()
} else if controllerType == "ThirdParty" {
configureThirdPartyController()
} else {
showControllerError()
}
3. Keep Conditions Readable:
// Good
let isValidUser = hasAccount && isEmailVerified && !isBanned
if isValidUser {
allowLogin()
}
// Less readable
if hasAccount && isEmailVerified && !isBanned {
allowLogin()
}
Summary
Control flow is what brings your Swift programs to life. Here’s what we’ve covered:
- Conditional statements (
if
,else
,guard
) for making decisions - Switch statements for powerful pattern matching
- Loops (
for-in
,while
,repeat-while
) for repeating operations - Loop control (
break
,continue
) for fine-tuned execution - Best practices for writing clean, readable control flow
Master these concepts, and you’ll be able to build apps that respond intelligently to user input, process data efficiently, and handle complex business logic with confidence.
Happy coding!