Swift Control Statements

Swift Control Statements

A quick introduction to control statements in Swift language.

ยท

12 min read

Swift is a Turing complete and multi-paradigm programming language developed by Apple. This means:

Turing complete: Swift has the ability to simulate a Turing machine, meaning it can compute anything that is computable. This is because Swift has:

  • Unbounded memory: Variables can store values of any size.

  • Conditional branching: if/else and switch statements

  • Loops: for, while and repeat-while loops

These features allow Swift to manipulate data in arbitrary ways, making it Turing complete.

Multi-paradigm: Swift supports multiple programming paradigms including:

  • Procedural: Functions are first-class citizens and can be passed around

  • OOP: Classes, inheritance, encapsulation

  • Functional: Closures, map/filter/reduce higher-order functions

Control statements: All structured programming languages require some basic control statements to control the flow of execution. These include:

  • Sequential execution: Statements are executed one after the other

  • Conditional execution: if/else, switch - only certain code blocks are executed based on conditions

  • Repeated execution: for, while, repeat-while - execute a code block repeatedly until a condition is met.

Control statements allow us to make decisions and repeat tasks in our code, making it flexible and reusable.


Decision statements

Statements like if/else, switch, and guard that allows the code to make decisions based on conditions are called decision statements. They control the flow of execution by branching the code in different directions.

Branching:

When a condition evaluates to true, one branch of code is executed. Otherwise, the alternative branch is executed. This is called branching. For example:

if condition {
   // True branch  
} else {
   // False branch
}

Forking:

When multiple conditions are checked and multiple branches of code can be executed, it is called forking. For example:

if condition1 {
   // Branch 1
} else if condition2 {
   // Branch 2
} else {
   // Default branch
}

Selection:

When conditions are sequentially checked one after the other, it is called a ladder or selection. The switch statement is an example of this type of selector:

switch expression {
    case value1:
        // Code for value1  
    case value2:         
        // Code for value2
    default:
        // Default code     
}

In summary, decision statements allow code to branch and fork based on conditions, controlling the flow of execution. They are an essential part of any structured programming language.


Fallthrough

The fallthrough keyword in Swift allows a case in a switch statement to fall directly to the next case. This is useful when you want to handle multiple cases in the same way.

For example:

let character: Character = "a"

switch character {
case "a", "e", "i", "o", "u":
    print("\(character) is a vowel.")
    fallthrough     
default:    
    print("\(character) is a consonant.")
}
// Prints "a is a vowel." 
// Then falls through to print "a is a consonant."

Here we want to treat 'a', 'e', 'i', 'o' and 'u' as vowels but also fallthrough to the default case to print that they are consonants.

Without fallthrough :

switch character {
case "a":
    print("\(character) is a vowel.")
case "e":     
    print("\(character) is a vowel.")     
// ...    
default:    
    print("\(character) is a consonant.")
}

We would have to repeat the vowel-checking code for each case.

So fallthrough allows us to execute the code for the next case after the current one, avoiding code repetition. It's useful when multiple cases share some common functionality and then diverge.

Notes:
Swift's switch statement design is different from other languages like C and Java in a few ways:

  1. Swift uses fallthrough instead of break - In C and Java, you have to explicitly use the break statement to break out of a case. If you omit break, it will fall through to the next case by default.

In Swift, the opposite is true. Cases break automatically, unless you use fallthrough to explicitly fall through to the next case.

  1. Breaking is the default behavior - In Swift, when a case's code block finishes execution, it breaks out of that case by default. You have to use fallthrough to continue to the next case.

This makes the code more readable since the normal/expected behavior is breaking, and you only fall through when you explicitly want to.

  1. No implicit fallthrough - In C and Java, if you omit the break statement, it implicitly falls through to the next case.

In Swift, you have to use the fallthrough keyword explicitly to fall through to the next case. There is no implicit fallthrough.

This makes the code more explicit and avoids accidental fallthrough.

  1. No dangling fallthroughs - If you use fallthrough in Swift but there is no next case, it will result in a compiler error. This ensures there are no "dangling" fallthroughs.

So in summary, Swift's switch statement:

  • Uses fallthrough instead of break

  • Has breaking as the default behavior

  • Requires explicit fallthrough (no implicit fallthrough)

  • Ensures no dangling fallthroughs

This makes the switch statement safer and more readable in Swift compared to languages that use break and have implicit fallthrough.


Repetition

Here are the different repetitive block statements in Swift with code examples and comments:

for-in: Loops through items in a collection.

let names = ["John", "Jack", "Jane"]
for name in names {
    print(name) 
}
// Prints John, Jack, Jane

while: Loops while a condition is true.

var i = 0
while i < 10 {
    print(i)
    i += 1
}
// Prints 0 - 9

repeat-while: Loops and then checks condition. Runs at least once.

repeat {
    print("Hello")
} while false

// Prints Hello once

.times: Loops a certain number of times.

3.times {
    print("Hello") 
}

// Prints Hello 3 times

.forEach: Loops through a collection.

[1,2,3].forEach { number in
    print(number)
}

// Prints 1, 2, 3

.map: Loops and applies a transform to each element.

[1,2,3].map { $0 * 2 } 
// [2, 4, 6]

In summary:

  • for-in: Loops through a collection

  • while: Loops while a condition is true

  • repeat-while: Loops and then checks condition

  • .times: Loops a fixed number of times

  • .forEach: Applies a closure to each element

  • .map: Applies a transform and returns a new collection

The .times, .forEach and .map methods are functional alternatives to the traditional for/while loops.


Range

There are a few ways you can iterate a range of numbers in Swift:

  1. for-in loop:
for i in 1...10 {
    print(i)
}
// Prints 1 2 3 4 5 6 7 8 9 10

Here we use the ..< or ... range operators to define a range, and then iterate over it with a for-in loop.

  1. while loop:
var i = 1
while i <= 10 {
    print(i)
    i += 1
}
// Prints 1 2 3 4 5 6 7 8 9 10

We initialize a variable, check the condition, and increment the variable in the loop.

  1. stride():
for i in stride(from: 1, to: 11, by: 2) {
    print(i) 
}
// Prints 1 3 5 7 9

The stride() function iterates over a range with a specific step. Here we use a step of 2, so it prints odd numbers.

  1. .forEach:
(1...10).forEach { number in
    print(number)
}
// Prints 1 2 3 4 5 6 7 8 9 10

The forEach() method iterates over a range and applies a closure to each element.

So in summary, you can iterate a range of numbers in Swift using:

  • for-in loop

  • while loop

  • stride() function

  • .forEach method


Interruptions

Label: A label allows you to identify a loop so you can break or continue from that specific loop.

Break: The break statement exits the entire loop (labeled or not).

Continue: The continue statement exits the current iteration of the loop and continues with the next iteration.

Example with nested loops and labels:

outerLoop: for i in 1...3 { 
   for j in 1...3 {
       if i == 2 && j == 2 { 
           break outerLoop
       }
       print("i is \(i), j is \(j)")
   }
}
// i is 1, j is 1 
// i is 1, j is 2
// i is 1, j is 3
// i is 2, j is 1

Here we have 2 nested for loops. We label the outer loop outerLoop.

When i is 2 and j is 2, we call break outerLoop, which breaks out of the outer labeled loop, instead of just the inner loop.

So in summary, labels allow you to:

  • Break out of specific loops using break <label name>

  • Continue specific loops using continue <label name>

This is useful for nested loops, where you may want to break out of the outer loop, instead of just the inner loop.

The example shows:

  • Two nested for loops

  • A label outerLoop applied to the outer loop

  • A break statement calling break outerLoop to break out of the outer labeled loop


Guard

The guard statement in Swift helps you validate conditions early and exit the current scope if the conditions fail. Here are some use cases for guard:

  1. Optional Binding:
guard let name = person.name else {
    print("No name available!")
    return 
}

print("Name is \(name)")

Here we use guard to optionally bind person.name and exit the scope if it's nil.

  1. Checking Conditions:
guard number > 10 else {
    print("Number is too small")
    return 
}

print("Number is greater than 10")

Here we use guard to check if a number is greater than 10, and exit the scope if it's not.

  1. Force Unwrapping:
guard let name = nameTextfield.text else {
    print("Name field is empty!")
    return
}

// Use name variable

Here we force unwrap an optional string from a text field using guard. If it's nil, we exit the scope.

  1. Checking Function Arguments:
func greet(person: String) {
    guard !person.isEmpty else {
        return 
    }

    print("Hello, \(person)!")
}

Here we use guard to check if a function argument is empty, and exit the function if it is.

In summary, guard statements allow you to:

  • Optionally bind values

  • Check conditions

  • Force unwrap optionals

  • Validate function arguments

If the guard condition fails, the guard statement exits the current scope. This helps exit early and clean up the code by avoiding nested if/else statements.


Assertions

Assertions work similarly to assertions in other C-based languages like C and Objective-C. You can use assertions in Swift like this:

assert(condition, "Failure message")

If the condition evaluates to true, the assertion succeeds and nothing happens.

If the condition evaluates to false, the assertion fails and the failure message string is printed. This will also result in a runtime error, terminating the program.

For example:

let age = 10

assert(age >= 18, "Age must be at least 18")  // Assertion fails

This will print:

Age must be at least 18
Fatal error: file SwiftAssertions.swift, line 3

And the program will terminate.

Some things to note about assertions in Swift:

  • They are only checked in debug builds, not in release builds.

  • They fail loudly with a runtime error when the condition fails.

  • They are used to check for programmer errors or invalid states that should never occur.

  • The failure message string is optional.

So in summary, assertions in Swift:

  • Check a condition

  • Fail loudly in debug builds only (not release builds)

  • Fail with a runtime error and termination

  • Are used to check for invalid program states

  • Have an optional failure message string

Hope this explanation of assertions in Swift helps! Let me know if you have any other questions.

Use-cases

Here are some common use cases for assertions in Swift:

  1. Validate function arguments:
func calculateArea(width: Int, height: Int) -> Int {
    assert(width > 0, "Width must be greater than 0")
    assert(height > 0, "Height must be greater than 0")

    return width * height
}

Here we assert that the width and height arguments are greater than 0. This ensures the function will only be called with valid arguments.

  1. Check for unexpected null values:
let name = person.name

assert(name != nil, "Name should not be nil")

Here we assert that an optional string should not be nil, to catch any unexpected null values.

  1. Ensure preconditions are met:
func transferFunds(from: BankAccount, to: BankAccount, amount: Int) {

    assert(from.balance >= amount, "Insufficient funds")

    // Transfer funds
}

Here we assert that the from account has enough balance to transfer the given amount. This ensures our preconditions are met before processing the transfer.

  1. Check for invalid states:
assert(isUserAuthenticated, "User not authenticated")

Here we assert that the user is authenticated, to catch any invalid states where the user is not authenticated but certain actions are attempted.

  1. Detect logic errors during development:
let result = calculate()
assert(result != 0, "Calculation result should not be 0")

During development, assertions can detect logic errors that result in unexpected values.

In summary, assertions in Swift should be used:

  • To validate function arguments and preconditions

  • To detect invalid states and null values

  • To detect logic errors during development

  • Anywhere you want to fail loudly if an assumption about your code is broken

Notes: guard statements in Swift are similar to assertions. They have some similarities and differences:

Similarities:

  • They both validate conditions and exit the current scope if the condition fails.

  • They are used to ensure assumptions about the code are true.

Differences:

  • Assertions are checked in debug builds only, while guard statements are checked in all builds.

  • Assertions result in a runtime error if they fail, while guards exit the current scope gracefully.

  • Guards bind optionals to non-optional variables, while assertions do not.

  • Guards have a more graceful failure by exiting the scope, while assertions fail loudly.

So in summary:

  • Both assert and guard validate conditions.

  • Assertions only work in debug builds, guards work in all builds.

  • Assertions fail loudly with a runtime error, guards exit the scope gracefully.

  • Guards can optionally bind values, but assertions cannot.

Because of these differences:

  • Use assertions to check for programmer errors or bugs that should never happen.

  • Use guards to validate user input, function arguments, options etc. that may legitimately fail at runtime.

So while similar in some ways, guards and assertions serve different purposes due to how they handle failure - guards exiting scope gracefully and assertions failing loudly.


Disclaim: This article is my personal research executed with AI named Rix. I'm learning Swift by asking questions. If you find bugs, don't blame me. If you have learned something don't tell me. Just learn and prosper. ๐Ÿ––

ย