Control Flow and Program LogicIntroductionA Swift program is more than a sequence of statements. It must make decisions, repeat operations, reject invalid states, and stop safely when assumptions are violated. Control flow describes the order in which statements execute. Program logic describes the rules used to select that order. In iOS development, control flow appears everywhere: validating a login form, choosing a screen based on application state, iterating through products in a shopping cart, processing user permissions, and handling optional data returned by an API. Clear control flow improves correctness, readability, testing, and maintainability. Swift provides several complementary structures: - conditional statements such as
if and else; switch statements with pattern matching;- ranges for expressing ordered intervals;
for-in, while, and repeat-while loops;guard statements for validating required conditions;- early exits using
return, break, and continue; - assertions and preconditions for detecting programming errors.
The central design principle is to select the simplest structure that clearly expresses the algorithm. Learning ObjectivesBy the end of this chapter, you should be able to: - Explain how Swift evaluates Boolean conditions.
- Construct decisions using
if, else if, and else. - Use
switch pattern matching with values, ranges, tuples, and where clauses. - Distinguish closed, half-open, and one-sided ranges.
- Select suitable loop structures and control loop execution efficiently.
- Use
guard statements and early exits to create safe, readable functions. - Distinguish assertions, preconditions, and ordinary runtime error handling.
- Design a small algorithm using appropriate Swift control structures.
1. Boolean Logic and Conditional ExecutionA Boolean value has one of two states: true or false. Swift represents this type with Bool. let isLoggedIn = true
let hasPermission = false Conditions commonly use comparison operators: | Operator | Meaning |
|---|
== | equal to | != | not equal to | > | greater than | < | less than | >= | greater than or equal to | <= | less than or equal to | && | logical AND | | ` | | ! | logical NOT |
Swift uses short-circuit evaluation. In an expression joined by &&, evaluation stops as soon as one condition is false. In an expression joined by ||, evaluation stops as soon as one condition is true. This both improves efficiency and can prevent unsafe evaluation. if user != nil && user!.isActive {
print("Active user")
}Although the example is valid, force unwrapping is often avoidable. A safer approach is optional binding, discussed later with guard. The if StatementAn if statement executes a block only when its condition is true. let temperature = 23
if temperature > 30 {
print("Hot")
}An else block handles the alternative path. if temperature > 30 {
print("Hot")
} else {
print("Moderate or cool")
}Several mutually exclusive alternatives can be expressed with else if. if temperature < 10 {
print("Cold")
} else if temperature < 25 {
print("Comfortable")
} else {
print("Warm")
}The order matters. Once a condition is true, later branches are not evaluated. Conditions should therefore normally be ordered from the most specific or restrictive case to the more general case. Combining Conditionslet age = 22
let hasTicket = true
if age >= 18 && hasTicket {
print("Entry permitted")
}Parentheses can clarify intent, particularly when combining && and ||. if (isAdmin || isModerator) && hasTwoFactorAuthentication {
print("Privileged access")
}A condition should express a business or application rule, not merely make code compile. Poorly named Boolean expressions are difficult to review. A meaningful name often improves clarity: let canPurchase = userIsAuthenticated && hasPaymentMethod && itemIsAvailable
if canPurchase {
print("Proceed to checkout")
}2. switch and Pattern MatchingSwift's switch statement is particularly powerful for representing multiple alternatives. Unlike some languages, Swift requires a switch to be exhaustive: every possible value must be covered. A default case can cover values not explicitly listed. let statusCode = 404
switch statusCode {
case 200:
print("Success")
case 404:
print("Not found")
default:
print("Other response")
}A case does not automatically fall through to the next case. This prevents accidental execution of unrelated branches. Multiple Matching ValuesCases can contain several values separated by commas. let character = "a"
switch character {
case "a", "e", "i", "o", "u":
print("Vowel")
default:
print("Other character")
}Ranges in switchRanges are useful when categories are defined by intervals. let score = 82
switch score {
case 90...100:
print("A")
case 80..<90:
print("B")
case 70..<80:
print("C")
default:
print("Below C")
}Here, 90...100 includes both endpoints, while 80..<90 includes 80 but excludes 90. Value BindingA matched value can be bound to a constant or variable. let point = (3, -2)
switch point {
case let (x, y) where x == y:
print("On the diagonal: \(x), \(y)")
case let (x, y):
print("Point: \(x), \(y)")
}The where clause adds a condition to a pattern. This is useful when the general shape of a value is not sufficient to determine the result. Enumerations and Associated Valuesswitch is especially appropriate for enumerations.
enum NetworkState {
case loading
case success(String)
case failure(String)
}
let state = NetworkState.success("Profile loaded")
switch state {
case .loading:
print("Show spinner")
case .success(let message):
print(message)
case .failure(let message):
print("Error: \(message)")
}Each case communicates a distinct state. Exhaustive matching ensures that a newly added enumeration case is noticed by the compiler rather than silently ignored. fallthrough
Swift normally stops after the matching case. The fallthrough keyword deliberately continues into the next case, but it should be used sparingly because it can reduce clarity. let number = 1
switch number {
case 1:
print("One")
fallthrough
case 2:
print("One or two")
default:
print("Other")
}The output is One followed by One or two. In most application code, combining case patterns or extracting a function is clearer than using fallthrough. 3. RangesA range represents an ordered interval of values. Swift commonly uses three forms: | Syntax | Meaning | Example |
|---|
a...b | closed range, both endpoints included | 1...5 | a..<b | half-open range, upper endpoint excluded | 0..<5 | a... or ...b | one-sided range | 5..., ...5 |
A closed range 1...5 contains 1, 2, 3, 4, and 5. A half-open range 0..<5 contains 0, 1, 2, 3, and 4. Half-open ranges are especially useful for collection indices because the upper bound equals the collection count. let names = ["Ava", "Ben", "Chen"]
for index in 0..<names.count {
print(names[index])
}In modern Swift, direct iteration is usually clearer: for name in names {
print(name)
}One-sided ranges are useful for filtering and pattern matching. let values = [3, 8, 12, 19]
let largeValues = values.filter { $0 >= 10 }A range has an applicable element type. For example, integer ranges can be iterated, whereas a general floating-point range is not automatically a Sequence for for-in iteration. For floating-point intervals, use appropriate stride operations or comparisons. 4. Loops and RepetitionLoops repeat a block while a collection, range, or condition supplies work to perform. The choice of loop should communicate the algorithm's intent. for-in
Use for-in when processing each element of a sequence. let prices = [12.50, 8.00, 19.25]
for price in prices {
print(price)
}The loop can process a range: for number in 1...5 {
print(number)
}It can also iterate over a dictionary. Dictionary order should not be assumed unless the program explicitly establishes an ordering. let quantities = ["Pen": 4, "Book": 2]
for (item, quantity) in quantities {
print("\(item): \(quantity)")
}Use _ when the loop variable is intentionally unused: for _ in 1...3 {
print("Retry")
}while
A while loop evaluates its condition before each iteration. It may execute zero times. var attempts = 0
while attempts < 3 {
print("Attempt \(attempts + 1)")
attempts += 1
}The loop must make progress toward termination. Forgetting to update attempts creates an infinite loop. repeat-while
A repeat-while loop evaluates its condition after the body, so it executes at least once. var response: String
repeat {
response = "continue"
} while response == "retry"This structure is appropriate for menu systems or input operations that must occur before a decision is made. break and continue
break exits the nearest loop or switch. continue skips the remainder of the current loop iteration.
for number in 1...10 {
if number % 2 == 0 {
continue
}
print(number)
}This prints odd numbers. A break can terminate a search once a result is found: for name in names {
if name == "Ben" {
print("Found")
break
}
}For nested loops, labeled statements can identify the intended loop, but labels should be used only when they materially improve clarity. 5. guard and Early ExitsA guard statement requires its condition to be true for execution to continue. If the condition is false, its else block must exit the current scope using return, break, continue, or an appropriate terminating operation. func greet(userName: String?) {
guard let userName = userName else {
print("No name provided")
return
}
print("Hello, \(userName)")
}The successfully unwrapped userName remains available after the guard statement. This is a major difference from deeply nested if let structures. Multiple requirements can be combined: func displayProfile(name: String?, age: Int?) {
guard let name = name,
let age = age,
age >= 0 else {
print("Invalid profile")
return
}
print("\(name) is \(age) years old")
}A useful design pattern is to place invalid or exceptional cases near the beginning of a function and leave the main algorithm at the normal indentation level. func process(items: [String]?) {
guard let items = items, !items.isEmpty else {
return
}
for item in items {
print(item)
}
}guard is not a replacement for all error handling. If a failure should be reported to a caller, the function may use throws and return an error rather than silently returning.
6. Assertions, Preconditions, and Runtime ValidationAn assertion documents an assumption that should be true during development. If the assumption fails, Swift reports a programming error. Assertions are commonly disabled in optimized production builds. let percentage = 75
assert(percentage >= 0 && percentage <= 100, "Percentage must be between 0 and 100") Use assertions for conditions that indicate a defect in the program, not ordinary user input. User input should be validated with ordinary conditions and should receive a useful response. A precondition expresses a requirement that must hold even when assertions are disabled in optimized builds. func element(at index: Int, in values: [Int]) -> Int {
precondition(values.indices.contains(index), "Index out of range")
return values[index]
}The distinction is practical: | Mechanism | Purpose | Typical use |
|---|
if / guard | ordinary program decisions | invalid form input, missing data | assert | development-time invariant | internal algorithm assumption | precondition | mandatory caller or state requirement | impossible or unsafe API usage | throw | recoverable failure communication | network or file operation failure |
Assertions must not be used to perform essential side effects, because their evaluation may differ between build configurations. 7. Selecting an Appropriate Control StructureA useful selection framework is: - One condition: use
if or guard. - Several related alternatives: use
switch. - Each element of a sequence: use
for-in. - Unknown number of repetitions controlled by a condition: use
while. - At least one execution required: use
repeat-while. - Invalid input should stop the current scope: use
guard. - A violated internal invariant: use
assert or precondition.
Avoid replacing clear logic with overly compact expressions. Nested ternary operators, excessive Boolean negation, and long chains of unrelated conditions may be syntactically valid but difficult to test and maintain. A control structure should also reflect the data model. An enumeration with associated values is generally clearer than a collection of unrelated status strings. A switch over an enumeration allows the compiler to help maintain completeness when the model evolves. 8. Applied Case: Validating a Mobile CheckoutThe following is an illustrative case for an iOS shopping application. BackgroundA checkout screen receives an optional customer name, a cart of products, and a payment status. The application must determine whether checkout can proceed. struct Product {
let name: String
let price: Double
let stock: Int
}
enum PaymentStatus {
case notStarted
case authorized
case declined
}ProblemThe application must reject missing customer information, an empty cart, products with insufficient stock, and payments that have not been authorized. It should provide a clear message for each failure. AnalysisThe validation rules are sequential prerequisites. guard is suitable for prerequisites because each failure exits early. The cart requires iteration, so for-in is appropriate. Payment status has a finite set of alternatives, so switch is appropriate. Solutionfunc checkout(customerName: String?,
cart: [Product],
payment: PaymentStatus) -> String {
guard let customerName = customerName,
!customerName.trimmingCharacters(in: .whitespaces).isEmpty else {
return "Enter a customer name."
}
guard !cart.isEmpty else {
return "Your cart is empty."
}
for product in cart {
guard product.stock > 0 else {
return "\(product.name) is out of stock."
}
}
switch payment {
case .notStarted:
return "Authorize payment before checkout."
case .declined:
return "Payment was declined."
case .authorized:
return "Checkout approved for \(customerName)."
}
}OutcomeThe function has a visible normal path and places failure conditions close to their causes. It avoids force unwrapping and does not continue processing after a known failure. Learning Pointsguard is effective for prerequisite validation.for-in expresses inspection of every product.switch communicates mutually exclusive payment states.- The enumeration prevents arbitrary status strings and supports exhaustive checking.
- User-facing validation is preferable to assertions because these failures are expected during ordinary use.
9. Challenges, Limitations, and Professional PracticeControl flow can become difficult when requirements grow. Deep nesting often indicates that validation should be separated into functions or represented by stronger types. A function containing many unrelated branches may violate the single-responsibility principle. Loops can also have performance implications. A linear scan through n items generally performs work proportional to the number of items, commonly described as O(n). This notation is a general growth description, not a precise execution-time prediction; actual performance depends on hardware, collection type, and operations inside the loop. For certificate-level iOS work, the main practical lesson is to avoid unnecessary repeated scans and to choose collection APIs that express intent. Control-flow decisions should be tested at boundaries. For a score categorized as 80..<90, test values such as 79, 80, 89, and 90. Boundary testing is especially important for ranges, authentication rules, stock quantities, and age restrictions. Accessibility and user experience also matter. A validation branch should produce a message that explains what the user can do next. Security-sensitive logic must not rely solely on client-side control flow; authorization and payment decisions must ultimately be verified by trusted server-side systems. Best Practices- Prefer positive, clearly named conditions.
- Use
switch for finite states and enums. - Use half-open ranges for index-style iteration when appropriate.
- Use
guard to reject invalid prerequisites early. - Avoid force unwrapping unless safety is certain and documented.
- Keep loop bodies small and ensure loops can terminate.
- Use assertions for programmer assumptions, not ordinary user errors.
- Test both sides of every condition and every range boundary.
- Refactor deeply nested logic into focused functions.
- Preserve business and security rules on trusted servers where necessary.
Chapter SummaryControl flow determines the execution path of a Swift program. if statements represent conditional choices, while switch provides exhaustive alternatives and sophisticated pattern matching. Ranges express intervals and are useful in iteration and classification. for-in is appropriate for sequences, while for condition-controlled repetition, and repeat-while when one execution is required. break and continue control loop termination and iteration. guard supports early validation and keeps the normal path readable. Assertions and preconditions document assumptions, but they must be distinguished from ordinary validation and recoverable errors. Effective Swift programmers choose structures that match the problem, test boundary conditions, avoid unnecessary nesting, and treat control flow as part of application safety, usability, and maintainability.
Key TermsBoolean expression: An expression that evaluates to true or false. Branch: One possible path through a conditional structure. Conditional statement: A statement that selects execution based on a condition. Control flow: The order in which program statements are executed. Early exit: Leaving a scope before reaching its final statement. Exhaustive switch: A switch that covers every possible input value. Fallthrough: Deliberate continuation from one switch case to the next. Guard statement: A statement requiring a condition to be true for execution to continue. Half-open range: A range that includes its lower bound but excludes its upper bound. Invariant: A condition expected to remain true during program execution. Iteration: One execution of a loop body. Pattern matching: Comparing a value against structural or value-based patterns. Precondition: A requirement that must be true before an operation proceeds. Range: An interval of ordered values. Short-circuit evaluation: Stopping Boolean evaluation once the result is known. Statement: An instruction that performs an action or controls execution. Switch statement: A multi-branch control structure based on matching cases. Assertion: A development-oriented check of an assumed condition. Review QuestionsShort Answer Questions- What is the difference between
&& and || in Boolean expressions? - Why must a Swift
switch statement be exhaustive? - What is the difference between
1...5 and 1..<5? - Why is
guard useful for optional binding? - When should an assertion be used instead of ordinary input validation?
Long Answer Questions- Explain the major differences among
if, switch, and guard, providing a Swift example of each. - Compare
for-in, while, and repeat-while loops and identify an appropriate use case for each. - Explain Swift range types and discuss why half-open ranges are useful for collection indexing.
- Describe how pattern matching with tuples, ranges, and
where clauses improves program logic. - Discuss the professional risks of deeply nested conditionals, force unwrapping, and non-terminating loops.
Analytical / Application Questions- Design a Swift function that accepts an optional username and password and returns a meaningful validation message. Use
guard for prerequisite checks. - A delivery application classifies distances as local for 0–5 kilometres, regional for more than 5 and up to 50 kilometres, and extended above 50 kilometres. Write a
switch using appropriate ranges and explain the boundary choices. - Given an array of inventory items, design an algorithm that stops immediately when an item has zero stock, skips discontinued items, and reports success only if all remaining items are available. Identify where
break, continue, or early return should be used.
Multiple Choice Questions with AnswersQuestion 1Which statement is true about a Swift switch statement? A. It must always include a default case. B. It must be exhaustive. C. It automatically falls through after every case. D. It can only compare integer values. Correct Answer: B. It must be exhaustive. Question 2What values are included in the range 0..<4? A. 0, 1, 2, 3 B. 1, 2, 3, 4 C. 0, 1, 2, 3, 4 D. 1, 2, 3 Correct Answer: A. 0, 1, 2, 3 Question 3What is the primary purpose of a guard statement? A. To repeat a block indefinitely B. To require a condition and exit when it is false C. To make a switch non-exhaustive D. To disable compiler checking Correct Answer: B. To require a condition and exit when it is false Question 4Which loop always executes its body at least once? A. for-in B. while C. repeat-while D. switch Correct Answer: C. repeat-while Question 5What does continue do inside a loop? A. Exits the entire function B. Exits the loop permanently C. Skips the rest of the current iteration D. Repeats the previous iteration Correct Answer: C. Skips the rest of the current iteration Question 6Which construct is most appropriate for handling the cases of an enumeration? A. switch B. repeat-while C. assert D. fallthrough alone Correct Answer: A. switch Question 7What does the range 5...7 contain? A. 5 and 6 only B. 6 and 7 only C. 5, 6, and 7 D. 5 through 8 Correct Answer: C. 5, 6, and 7 Question 8Which mechanism is primarily intended for detecting a programming assumption during development? A. assert B. print C. for-in D. continue Correct Answer: A. assert Question 9What happens when a break statement is executed inside a loop? A. The current iteration is skipped and the loop continues. B. The nearest loop ends. C. The program always terminates. D. The loop condition becomes true. Correct Answer: B. The nearest loop ends. Question 10Why is direct iteration such as for name in names often preferable to indexing through 0..<names.count? A. It changes the collection into a dictionary. B. It avoids expressing unnecessary index logic. C. It guarantees alphabetical ordering. D. It permits mutation of every collection. Correct Answer: B. It avoids expressing unnecessary index logic.
|