Learn Swift: The Complete Beginner’s Guide (Zero Experience Required)

A structured, lesson-by-lesson guide to learning the Swift programming language. Each lesson takes about 30 minutes, includes real code you can run, and ends with a challenge to test yourself.
Written by

Chris C

Updated on

Apr 03 2026

Table of contents
    Full Curriculum

    Each stage builds on the previous one. Work through them in order and you’ll have a solid Swift foundation ready for real app development.

    Learn Swift Stage 1: Swift Introduction

    Every Swift developer started exactly where you are right now. These five lessons will take you from “I’ve never written a line of code” to writing real Swift programs you can run yourself.

    You don’t need any prior coding knowledge to follow along. You don’t need a computer science degree. You just need Xcode (Apple’s free app for Mac) or Swift Playgrounds (free on iPad or Mac). Every lesson is designed to fit in about 30 minutes and ends with a small challenge so you can test what you just learned.

    By the end of Stage 1, you’ll understand how Swift programs work, how to store information in your code, and how to make your programs display different kinds of output. That might sound simple, but it’s the actual foundation everything else is built on.

    01
    Stage 1
    Swift Introduction
    5 lessons · ~2.5 hrs
    1.1
    What is Swift and Where Do You Write It?
    ⏱ ~30 min 🛠 Setup lesson

    What is Swift?

    Swift is a programming language made by Apple. A programming language is simply a way to give instructions to a computer. You write those instructions in a specific format the computer understands, and the computer follows them.

    Think of it like a recipe. A recipe is written in a specific format — ingredients first, then steps in order — because that’s how a cook expects to read it. Swift is the same idea: a specific format for writing instructions that a computer can read and follow.

    Apple uses Swift to build iOS apps, Mac apps, Apple Watch apps, and more. When you learn Swift, you’re learning the same language that Apple’s own engineers use every day.

    Where do you actually write Swift?

    You have two good options as a beginner, and both are free:

    Swift Playgrounds Best for getting started fast
    A free app from Apple available on iPad and Mac. You can write Swift code and see the results immediately, with no setup. If you’re on an iPad or just want to start as quickly as possible, this is the way to go. Download it from the App Store for free.
    Xcode The full development tool you’ll eventually use
    Xcode is Apple’s full app-building tool for Mac. It’s free and available on the Mac App Store. When you’re ready to build real iOS apps, Xcode is what you’ll use. For these early lessons, you’ll use its built-in “Playground” feature to run Swift code without building a full app.

    For the rest of Stage 1, instructions will say “open a Playground.” In Xcode: go to File → New → Playground, choose “Blank,” and you’re ready. In Swift Playgrounds on iPad: tap the + button and create a new blank playground.

    What does a Swift program look like?

    Here’s the simplest possible Swift program. This is real, working code:

    print("Hello, world!")
    Output Hello, world!

    That one line tells the computer: display the text “Hello, world!” on the screen. When you type it into a Playground and run it, that’s exactly what you’ll see.

    PartWhat it means
    printThis is a built-in Swift instruction that means “show something on the screen.” It’s called a function, but don’t worry about that word yet — just know that print displays output.
    ( )The parentheses are where you put whatever you want to display. Everything inside the parentheses gets shown.
    "Hello, world!"The quotation marks tell Swift “this is text, not code.” The words between the quotes are displayed exactly as written.
    Try it now: Open a Playground, type that one line exactly, and press the run button (or Shift+Return). You should see “Hello, world!” appear in the output area. If you do — congratulations, you just ran your first Swift program.

    How Swift runs your code

    When you hit run, Swift reads your code from top to bottom, one line at a time, and executes each instruction in order. If you have three lines of code, it runs line 1 first, then line 2, then line 3. This is important to remember as your programs get longer.

    Common beginner mistake: Forgetting the quotation marks around text. If you write print(Hello) instead of print("Hello"), Swift will show an error because it tries to look up something called Hello in your code rather than displaying the word as text. Quotation marks = text. No quotation marks = something defined in your code.
    🎯 Your Challenge
    Make Swift say something about you
    Write a Swift program that prints three lines of output: your name on the first line, your city on the second line, and why you want to learn Swift on the third line. Run it and make sure all three lines appear.

    You’ll need three separate print() statements, each on its own line. Try it without looking anything up first.
    Hint: Swift runs code top to bottom. Each print() goes on its own line and produces its own line of output.

    Go deeper with AI

    Deepen Your Understanding Use AI as a tutor — no code generation
    I just learned what Swift is and ran my first print() statement. Can you explain in plain English: what is actually happening when I press “run” in a Swift Playground? What does the computer do with my code?
    I’m a complete beginner learning Swift. What’s the difference between Swift, Xcode, and Swift Playgrounds? I keep seeing all three terms and I’m not sure which is which.
    Build a Practice Example Get a commented example you can study and run
    Write a Swift Playground example that uses print() in 5 different ways — printing text, numbers, multiple things at once, and empty lines. Add a comment on every line explaining what it does. Write the comments for someone who has never coded before.
    1.2
    print() — Displaying Output from Your Code
    ⏱ ~30 min 📦 Built-in functions

    You already used print() in the last lesson. Now let’s understand it properly. It’s the most important tool you have as a beginner because it lets you see what’s happening inside your code.

    print() does more than show text

    You can print more than just words. You can print numbers, the result of a calculation, or even multiple things at once. Try all of these in a Playground:

    // Printing text (words need quotation marks)
    print("I am learning Swift")
    
    // Printing a number (numbers don't need quotation marks)
    print(42)
    
    // Printing the result of a calculation
    print(10 + 5)
    
    // Printing multiple things separated by a comma
    print("The answer is", 42)
    
    // Printing a blank line (empty parentheses)
    print()
    Output I am learning Swift 42 15 The answer is 42
    What you wroteWhat Swift does with it
    print("I am learning Swift")Displays the text exactly as written, because it’s in quotation marks.
    print(42)Displays the number 42. No quotes needed for numbers.
    print(10 + 5)Swift calculates 10 + 5 first, gets 15, and displays that result.
    print("The answer is", 42)When you separate items with a comma, Swift prints them both on the same line with a space between them.
    print()Prints nothing, which creates a blank line. Useful for spacing output.

    Comments: notes in your code

    You saw those lines starting with // in the code above. Those are called comments. They’re notes you write for yourself (or other developers) to explain what the code does. Swift completely ignores anything after // on a line.

    // This whole line is a comment — Swift ignores it entirely
    print("This line runs") // This part is a comment, but print() still runs
    Output This line runs

    Get into the habit of adding comments to your code. Beginners often skip them, but they’re genuinely helpful when you come back to code you wrote a week ago and can’t remember what it was doing.

    Basic math you can use inside print()

    Swift understands the standard math operators you already know:

    print(10 + 3)   // Addition → 13
    print(10 - 3)   // Subtraction → 7
    print(10 * 3)   // Multiplication → 30
    print(10 / 3)   // Division → 3 (not 3.33... — we'll explain this soon)
    print(10 % 3)   // Remainder → 1 (10 divided by 3 leaves remainder 1)
    Output 13 7 30 3 1
    Why does 10 / 3 equal 3? When you divide two whole numbers in Swift, you get a whole number back — the decimal part gets dropped. This surprises beginners. In the next lesson you’ll learn about different number types that preserve decimals.
    Use print() to debug: Professional developers use print() constantly to check what their code is doing. When something goes wrong, adding print() statements to see what values you have at different points is one of the most effective debugging techniques there is.
    🎯 Your Challenge
    A mini receipt printer
    Imagine you bought 3 items: one costs $12, one costs $8, and one costs $25. Write a Swift program that prints out each price on its own line, then prints a blank line, then prints the total. Use print() and the + operator to calculate the total — don’t just type the number manually.
    Hint: You can put a math expression directly inside print(). Try print(12 + 8 + 25) and see what happens.
    Deepen Your Understanding Solidify your understanding of print() without just copying code
    I just learned about print() in Swift. Can you quiz me on it? Ask me questions one at a time about what print() does in different situations. Tell me if I get something wrong and explain why.
    I’m learning Swift and I understand that // starts a comment. Can you explain when I should actually use comments in my code? Give me examples of good comments vs. useless comments.
    Build a Practice Example Get a commented example to study
    Write me a short Swift Playground example that shows 6 different things you can print: text, a number, a calculation, multiple items with a comma, an empty line, and something creative. Add a comment on every single line explaining what it does. Write for a complete beginner.
    1.3
    Variables and Constants: Storing Information in Your Code
    ⏱ ~30 min 🔑 Core concept

    So far your code has used values directly — you typed the number 42, you typed “Hello, world!”. But real programs need to store information and use it in multiple places. That’s what variables and constants are for.

    Think of it like a labeled box

    Imagine a cardboard box with a label on it. You put something inside the box, and whenever you need it, you refer to it by the label instead of by what’s inside. Variables and constants work exactly the same way. You give a piece of information a name, and you use that name to refer to it throughout your code.

    var: a box whose contents can change

    // Create a variable called "score" and put the number 0 inside it
    var score = 0
    
    // Use it — print() now uses the value stored in "score"
    print(score)   // 0
    
    // Change the value stored in "score"
    score = 10
    print(score)   // 10
    
    // You can do math with a variable
    score = score + 5
    print(score)   // 15
    Output 0 10 15
    LineWhat it does
    var score = 0Creates a new variable named score and gives it the value 0. The word var is how you tell Swift “I’m creating a variable.”
    print(score)Instead of printing a literal value, Swift looks up what’s stored in score and prints that. Notice: no quotation marks around score, because score is a variable, not text.
    score = 10Replaces what’s in the box. The old value (0) is gone, the new value (10) is stored.
    score = score + 5Read the right side first: get the current value of score (10), add 5, get 15. Then store 15 back into score. This pattern is extremely common in Swift.

    let: a box that’s sealed shut

    Sometimes you have information that should never change. Like your date of birth, or the number of days in a week. For those, you use let instead of var.

    // A constant — this value will never change
    let daysInWeek = 7
    
    print(daysInWeek)   // 7
    
    // This would cause an error — you can't change a constant:
    // daysInWeek = 8   ← Swift will refuse to compile this
    Output 7

    If you try to change a constant, Swift won’t let your code run at all. It will show an error that says something like “cannot assign to value: ‘daysInWeek’ is a ‘let’ constant.” This is actually a feature — Swift is protecting you from accidentally changing something you didn’t mean to change.

    var vs let: which to use? A good rule for beginners: start with let. If Swift complains that you need to change the value, switch it to var. Experienced Swift developers prefer let whenever possible because it makes code safer and easier to reason about.

    Naming your variables

    Variable names can be almost anything, but there are a few rules and conventions to follow:

    // Good names — clear and descriptive
    var playerName = "Alex"
    var highScore = 1500
    let maxLives = 3
    
    // Swift uses "camelCase" — no spaces, each new word starts with capital
    var numberOfItemsInCart = 0
    
    // These would cause errors:
    // var 1player = "Alex"    ← can't start with a number
    // var player name = "Alex" ← can't have spaces
    camelCase: Swift developers write multi-word variable names by starting lowercase, then capitalizing the first letter of each following word. Like playerName or highScore. This is the standard convention — if you follow it from the start, your code will look professional and be easy to read.
    🎯 Your Challenge
    Build a simple point tracker
    Create a variable called points and start it at 0. Then simulate a game where the player earns 10 points, then loses 3 points, then earns 7 more. After each change, print the current score. At the end, also use a constant to store the player’s name and print a final message.

    The output should show four different point values as the score changes, then a final line with the player name and their final score.
    Hint: Use points = points + 10 to add points. You can also write this as points += 10 — both do the same thing.
    Deepen Your Understanding Make sure you truly understand var vs let
    I just learned about var and let in Swift. Can you give me 5 real-world scenarios from an iOS app and ask me whether each one should use var or let? Quiz me one at a time and explain when I’m wrong.
    In Swift, why does it matter whether I use var or let if I’m the only one writing the code? Explain the practical reasons a beginner should care about this distinction.
    Audit Your Own Code Get feedback on the code you wrote for the challenge
    Here’s my Swift code from a challenge: [paste your code]. Can you check if I used var and let correctly? Are there any places where I used var but should have used let, or vice versa? Explain before suggesting changes.
    1.4
    Data Types: What Kind of Thing Are You Storing?
    ⏱ ~30 min 🔑 Core concept

    When you create a variable, Swift needs to know what kind of thing you’re storing. Is it text? A whole number? A number with a decimal? True or false? Each of those is a different data type, and Swift handles them differently.

    In the last lesson, you created variables without thinking about types — Swift figured it out automatically. Let’s understand what’s actually happening.

    The four most important data types

    String Text — any sequence of characters
    var name: String = "Aisha"
    var city = "Toronto"  // Swift infers this is a String
    var emptyText = ""    // An empty String is still a String
    A String is any text — words, sentences, even a single character. It always goes inside quotation marks. The name “String” comes from “a string of characters.”
    Int Whole numbers, positive or negative
    var age: Int = 28
    var score = 1500       // Swift infers this is an Int
    var temperature = -5  // Negative numbers work fine
    Int is short for “integer” — a whole number with no decimal point. Use it for things like scores, counts, ages, and quantities. It can be positive or negative.
    Double Numbers with decimal points
    var price: Double = 9.99
    var height = 1.75       // Swift infers this is a Double
    var pi = 3.14159       // Common math constants
    Double is for numbers that have (or might have) a decimal point. Use it for prices, measurements, percentages, and anything where fractions matter. This is why 10 / 3 gave you 3 earlier — both were Ints, so Swift did integer division.
    Bool True or false — only ever one of two values
    var isLoggedIn: Bool = false
    var hasCompletedLevel = true
    var isPremiumUser = false
    A Bool (short for “Boolean”) can only ever be true or false. It’s incredibly useful for tracking state in an app — is the user logged in? Has the player finished the level? Is this item favorited? You’ll use Booleans constantly once you start building apps.

    Type inference: Swift is smart

    You’ve probably noticed that in most examples you don’t need to write the type explicitly. That’s because Swift can figure it out from the value you assign:

    // With explicit type annotation (the part after the colon)
    var name: String = "Jordan"
    var score: Int = 100
    
    // Without — Swift infers the same types automatically
    var name2 = "Jordan"   // Swift sees quotes → String
    var score2 = 100       // Swift sees whole number → Int
    var price = 4.99       // Swift sees decimal → Double
    var done = true        // Swift sees true/false → Bool

    Both styles are correct. Beginners often leave out the type annotation since it’s less to type, and Swift handles it fine. You’ll want to add explicit types sometimes when Swift can’t infer correctly, but most of the time inference works perfectly.

    Why you can’t mix types

    Once a variable has a type, it keeps that type forever. You can’t store text in a variable that was created for numbers:

    var score = 100      // score is now an Int
    
    // This would cause an error:
    // score = "one hundred"  ← can't put a String in an Int variable
    
    // This is fine — still an Int:
    score = 200
    Cannot convert String to Int: This is one of the most common error messages beginners see. It means you’re trying to put the wrong type of value into a variable. Check what type your variable is, and make sure the value you’re assigning matches.
    🎯 Your Challenge
    Model a user profile with data types
    Create a set of variables and constants to represent a fictional user in an app. You need to store: their name, their age, their account balance, and whether they are a premium member. Use the most appropriate type for each one, and decide whether each should be var or let. Then print all four values.
    Hint: Think about which values might change over time (var) and which would stay constant (let). Age changes each year, so maybe that’s var. A username might stay the same, so maybe let. There’s no single right answer — think about what makes sense for your app.
    Deepen Your Understanding Understand why types matter in a real app
    I just learned about Swift data types: String, Int, Double, and Bool. Can you give me 10 things you’d find in a real iOS app — like a recipe app or fitness tracker — and ask me which data type each one should be stored as? Quiz me one at a time.
    In Swift, why can’t I mix types — like putting a String into an Int variable? Explain this to me like I’m a complete beginner. What problem does this strict type system actually solve?
    1.5
    String Interpolation: Mixing Variables and Text
    ⏱ ~30 min ⭐ Satisfying concept

    You know how to print text. You know how to print variables. Now it’s time to combine them — put the value of a variable right inside a sentence of text. This technique is called string interpolation, and once you learn it, you’ll use it in almost every Swift program you write.

    The problem it solves

    Imagine you want to print “Your score is 150” where 150 comes from a variable. Without string interpolation, you’d have to do something awkward like this:

    var score = 150
    
    // Awkward way — using a comma to join things
    print("Your score is", score)   // Your score is 150 ✓ but limited

    The comma approach works but it’s clunky — you can’t control spacing well, and it gets messy with complex sentences. String interpolation gives you a much cleaner way.

    String interpolation: \( ) inside a string

    To drop a variable’s value inside a string, you write a backslash followed by parentheses, with the variable name inside: \(variableName)

    var score = 150
    var playerName = "Maya"
    
    // Use \( ) to drop the variable right into the string
    print("Your score is \(score)")
    print("Great job, \(playerName)!")
    print("\(playerName) has \(score) points.")
    Output Your score is 150 Great job, Maya! Maya has 150 points.
    PartWhat happens
    "Your score is \(score)"Swift sees the \(score) part, looks up the current value of score, and replaces \(score) with that value in the final output.
    \(playerName)Works the same way for Strings — the variable’s value gets inserted right where \(playerName) appears.
    The quotes around the whole thingThe entire thing is still a String — the quotation marks wrap everything, and \( ) sections inside get substituted with values.

    You can put expressions inside \( )

    It’s not just variable names you can put inside the parentheses. You can put any expression — a calculation, a combination, anything Swift can evaluate to a value:

    var items = 3
    var priceEach = 4.99
    
    // A calculation inside the interpolation
    print("Total: $\(items * priceEach)")
    
    // Combining two strings
    var firstName = "Jamie"
    var lastName = "Chen"
    print("Full name: \(firstName) \(lastName)")
    
    // Using it with Bool
    var isPremium = true
    print("Premium account: \(isPremium)")
    Output Total: $14.97 Full name: Jamie Chen Premium account: true
    String interpolation works with any type: You can use \( ) with Strings, Ints, Doubles, and Bools — Swift automatically converts whatever’s inside to text so it can be displayed. This is one of the most convenient things about Swift.

    A common gotcha: the backslash

    Easy to mistype: String interpolation uses a backslash followed by parentheses: \(variable). Not a forward slash, not just parentheses. The backslash tells Swift “this is not regular text — look up a value here.” If you see your output showing \(score) literally instead of the variable’s value, check that you used a backslash and not a forward slash.

    Quick reference: String interpolation patterns

    SyntaxWhat It Does
    “Hello \(name)”Inserts the value of the variable name into the string
    “\(a + b)”Calculates a + b and inserts the result
    “\(first) \(last)”Inserts two variables with a space between
    “$\(price)”You can add characters right before or after \( ) — the $ stays literal
    “\(isActive)”Works with Bool — inserts “true” or “false”
    🎯 Your Challenge
    Build a personalised app welcome screen
    Create variables for a user’s name, their current level (a number), their coin balance (a number), and whether they have notifications enabled (true/false). Then use string interpolation to print a welcome message that incorporates all four values in natural sentences — like you might see when you open an app. The output should read naturally, not like a list of values.
    Hint: Something like “Welcome back, [name]! You’re on level [level] with [coins] coins.” Use multiple print() statements if you want the message on separate lines.
    Deepen Your Understanding Make string interpolation feel second nature
    I just learned string interpolation in Swift. Can you give me 5 fill-in-the-blank Swift print() statements where I have to write the interpolation myself? Show me the variables and the desired output — I’ll write the print() line with \( ) to make it work.
    In Swift, what’s the difference between these two approaches to combining text and a variable: print(“Score: ” + String(score)) versus print(“Score: \(score)”)? Which is better and why?
    Build a Practice Example See string interpolation used in a realistic context
    Write me a Swift Playground example that simulates the data model of a simple recipe app — ingredients count, prep time, calories, recipe name, whether it’s vegetarian. Then use string interpolation to print a formatted recipe summary. Add a comment on every line for a beginner. Don’t use any Swift concepts beyond variables, data types, and string interpolation.

    Stage 1 Recap: Swift Introduction

    You’ve finished Stage 1 of learning Swift. Here’s what you now know how to do:

    • Write and run Swift code in Xcode Playgrounds or Swift Playgrounds
    • Use print() to display text, numbers, calculations, and multiple values
    • Add comments with // to explain your code
    • Create variables with var (changeable) and constants with let (fixed)
    • Work with four core data types: String, Int, Double, and Bool
    • Use string interpolation with \( ) to embed variable values inside text

    These are the absolute building blocks of the Swift programming language. Everything you’ll learn from here — conditions, loops, functions, structs — builds on exactly what you now know.

    Stage 2 is where things get interesting. You’ll teach your programs to make decisions with if/else and switch statements. That’s when your code stops being a simple sequence of steps and starts responding to different situations — which is what all real apps do.

    Learn Swift Stage 2: Making Decisions

    Every app you’ve ever used is constantly asking questions — and Stage 2 is where you learn how to ask them in code.

    All you need to follow along is Xcode and a fresh Swift Playground. No new setup required. Each of the five lessons in this stage takes about 30 minutes and ends with a hands-on challenge you solve yourself. The challenge is where the real learning happens, so don’t skip it.

    By the end of Stage 2, you’ll be able to compare values, branch your code based on conditions, combine multiple conditions in a single check, use switch statements to handle multiple cases cleanly, and write quick one-line decisions with the ternary operator. These are the tools that turn a passive list of instructions into a program that actually thinks.

    02
    Stage 2
    Making Decisions
    5 lessons · ~2.5 hrs
    2.1
    Comparison Operators
    ⏱ 25 min Swift Basics

    Before your code can make a decision, it needs a way to ask a question. Is this number bigger than that one? Are these two words the same? Is the score high enough to win? These are all comparisons, and they’re the foundation of every decision your app will ever make.

    Think about a bouncer at a venue checking IDs. Their job is simple: look at the age on the ID, compare it to the minimum age, and decide yes or no. Your code does the exact same thing. It compares two values and gets back a yes or a no.

    In Swift, that yes-or-no answer is a Bool — either true or false. You already know what a Bool is from Stage 1. Now you’ll see where they actually come from.

    let myAge = 20                        // Store an age as an Int
    let minimumAge = 18                   // The age requirement to compare against
    
    let isOldEnough = myAge >= minimumAge  // Is 20 greater than or equal to 18?
    print(isOldEnough)                     // true
    
    let isExactMatch = myAge == minimumAge  // Is 20 exactly equal to 18?
    print(isExactMatch)                    // false
    
    let isTooYoung = myAge < minimumAge    // Is 20 less than 18?
    print(isTooYoung)                      // false
    Output
    true
    false
    false
    LineWhat it does
    myAge >= minimumAgeChecks if 20 is greater than or equal to 18. It is, so the result is true. That Bool gets stored in isOldEnough.
    myAge == minimumAgeChecks if 20 is exactly 18. It isn’t, so the result is false.
    myAge < minimumAgeChecks if 20 is less than 18. It isn’t, so the result is false.
    let isOldEnough = ...The result of any comparison is just a Bool. You can store it in a constant exactly like any other value.
    Very common mistake: == (two equals signs) checks whether two values are equal. A single = assigns a value. Using one equals sign when you meant two is one of the most frequent beginner bugs in any programming language. Swift will give you a compiler error, which actually helps catch it early.

    All Six Comparison Operators

    == Equal to — are these two values exactly the same?
    print(5 == 5)            // true
    print(5 == 6)            // false
    print("hello" == "hello") // true — works with strings too
    print("Hello" == "hello") // false — Swift is case-sensitive
    Works with numbers, Strings, and Bools. Remember that capitalisation matters with strings — "Hello" and "hello" are not considered equal.
    != Not equal to — are these two values different?
    print(5 != 6)           // true — 5 and 6 are different
    print("cat" != "dog")   // true — they're not the same
    print(10 != 10)         // false — they're equal, so "not equal" is false
    The ! in != means “not”. So != reads as “not equal”. Useful for checking if something has changed or if a field is not empty.
    < Less than — is the left value smaller than the right?
    print(3 < 10)   // true — 3 is less than 10
    print(10 < 3)   // false — 10 is not less than 3
    print(5 < 5)    // false — equal values don't count as "less than"
    The left value must be strictly smaller. If both values are equal, this returns false — equal is not less than.
    > Greater than — is the left value larger than the right?
    print(10 > 3)   // true — 10 is greater than 3
    print(3 > 10)   // false — 3 is not greater than 10
    print(5 > 5)    // false — equal values don't count as "greater than"
    Common use: checking if a count is above zero or a score has crossed a threshold. Strictly greater — equal doesn’t qualify.
    <= Less than or equal to — smaller than, or exactly the same?
    print(3 <= 5)   // true — 3 is less than 5
    print(5 <= 5)   // true — equal counts here
    print(6 <= 5)   // false — 6 is neither less than nor equal to 5
    Think of it as “at most”. Passes if the left value is smaller than or the same as the right value.
    >= Greater than or equal to — larger than, or exactly the same?
    print(5 >= 5)   // true — exactly equal counts
    print(6 >= 5)   // true — 6 is greater than 5
    print(4 >= 5)   // false — 4 is neither greater than nor equal to 5
    Think of it as “at least”. This is probably the most common operator you’ll use — minimum age checks, minimum score thresholds, inventory counts. You’ll see it everywhere.
    Memory trick: The open end of the arrow always faces the smaller number. In 3 < 10, the arrow opens toward 3 (the smaller one). In 10 > 3, it opens toward 3 again. Once that clicks, you’ll never mix them up.

    Quick Reference

    OperatorWhat It Checks
    ==Are the two values exactly equal?
    !=Are the two values different?
    <Is the left value strictly less than the right?
    >Is the left value strictly greater than the right?
    <=Is the left value less than or equal to the right?
    >=Is the left value greater than or equal to the right?
    🏆 Challenge
    Password Strength Check

    Create a constant called password and set it to any string you like. Then create a constant called passwordLength that stores how many characters are in the password — strings have a .count property that gives you this number. Finally, create a Bool constant called isStrongEnough that is true when the password is at least 8 characters long. Print all three values.

    Hint: You can get the length of any string with myString.count. Which of the six operators means “at least”?
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    Explain Swift’s six comparison operators to me like I’m a total beginner. Use a real-world analogy that has nothing to do with coding — like comparing heights, prices, or ages. Then quiz me: give me a comparison expression like “7 > 3” and ask me what Bool it produces. Ask one at a time and tell me if I get one wrong.
    What is the difference between = and == in Swift? I keep mixing them up. Can you explain it in plain English without any jargon, and tell me what actually happens in Swift if I accidentally use = when I meant ==?
    Build a Practice Example Generate a heavily commented example to study
    Write a short Swift example that uses all six comparison operators (==, !=, <, >, <=, >=) in a realistic scenario like comparing game scores or shopping cart totals. Add a comment on every single line explaining what the comparison checks and what Bool result it produces. Write the comments for a complete beginner.
    2.2
    if / else if / else
    ⏱ 35 min Control Flow

    Now that you can compare values and get a true or false result, you need a way to actually do something based on that result. That’s what an if statement does. It lets your code take different paths depending on a condition.

    Think about a traffic light. When the light is green, cars go. When it’s yellow, they slow down. When it’s red, they stop. Each colour triggers a different action. An if statement works the same way: “if this condition is true, do this thing. Otherwise, do something else.”

    This is one of the most important ideas in all of programming. Every app you’ve ever used relies on if statements constantly — checking if you’re logged in, if a form is filled out, if a score is high enough. Once you understand this, you can start writing code that actually responds to things.

    let score = 75                     // A student's test score out of 100
    
    if score >= 90 {                    // Check if score is 90 or above
        print("Grade: A")               // Only runs if the condition above is true
    } else if score >= 80 {            // Only checked if the first condition was false
        print("Grade: B")               // Runs if score is 80 to 89
    } else if score >= 70 {            // Only checked if both previous conditions were false
        print("Grade: C")               // 75 is in range 70–79 — this runs
    } else {                            // Runs if none of the conditions above were true
        print("Grade: F")               // Covers anything below 70
    }
    Output
    Grade: C
    PartWhat it does
    if score >= 90 { }The first condition Swift checks. If it’s true, the code inside the curly braces runs and Swift skips everything else.
    else if score >= 80 { }Only evaluated if the first condition was false. You can chain as many else if blocks as you need.
    else { }The safety net. Runs when no other condition matched. Optional, but good practice so every possible value is handled.
    The curly braces { }Everything between { and } belongs to that condition. This group of lines is called a “block”.
    Key behaviour: Swift stops checking as soon as one condition is true. In the example above, once score >= 70 matched, the else block was skipped entirely. Exactly one branch ever runs — even if multiple conditions could technically be satisfied.

    The Three Forms of an if Statement

    if only Do something when true, otherwise do nothing at all
    let isRaining = true
    
    if isRaining {                       // A Bool is already true or false — no == needed
        print("Don't forget your umbrella!") // Only runs when isRaining is true
    }
    // If isRaining were false, nothing prints — and that's totally fine
    The simplest form. No else required if you only want something to happen when the condition is true. Notice you don’t write isRaining == true — a Bool can be used directly as the condition.
    if / else Do one thing when true, a different thing when false
    let isLoggedIn = false
    
    if isLoggedIn {
        print("Welcome back!")           // Runs when isLoggedIn is true
    } else {
        print("Please sign in.")         // Runs when isLoggedIn is false — this one prints
    }
    Exactly one of the two blocks will always run. Perfect for binary decisions: logged in or not, day or night, pass or fail. You’ll use this form constantly.
    if / else if / else Handle three or more possible outcomes
    let hour = 14                       // 2pm in 24-hour time
    
    if hour < 12 {
        print("Good morning!")           // Hours 0 through 11
    } else if hour < 18 {
        print("Good afternoon!")         // Hours 12 through 17 — hour 14 lands here
    } else {
        print("Good evening!")           // Hour 18 and above
    }
    Add as many else if branches as you need. Swift checks them top to bottom and stops the moment one matches.
    nested if An if statement inside another if statement
    let hasTicket = true
    let age = 16
    
    if hasTicket {                       // First check: do they have a ticket?
        if age >= 18 {                   // Second check — only runs if they have a ticket
            print("Enjoy the show!")
        } else {
            print("Sorry, adults only.")    // Has ticket but is under 18 — this prints
        }
    } else {
        print("No ticket, no entry.")
    }
    You can nest if statements inside each other. Indent carefully so you can see which closing } belongs to which block. In Lesson 2.3 you’ll see a cleaner way to handle situations that need multiple checks at once.
    Indentation tip: Xcode automatically indents your code when you press Enter after an opening brace. If your indentation looks off, select all your code and press Control+I to re-indent everything. Consistent indentation makes bugs much easier to spot.
    🏆 Challenge
    Weather Advice Generator

    Create a variable called temperatureC representing today’s temperature in Celsius. Write an if / else if / else chain that prints different advice based on the temperature: below 0 prints "Freezing! Bundle up.", 0 to 14 prints "Cold. Grab a jacket.", 15 to 25 prints "Nice weather today!", and anything above 25 prints "Hot! Stay hydrated.". Test by changing the value and running again.

    Hint: You’ll need four branches. Work from coldest to hottest — top to bottom. Remember Swift stops at the first true condition, so the order of your checks matters.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    I’m learning Swift if/else statements. Can you walk me through — step by step — what Swift actually does when it evaluates an if/else if/else chain? I want to understand the execution order, not just the syntax. Use a simple scenario like checking a number, and explain the logic rather than just writing the code for me.
    What’s the difference between writing three separate if statements versus one if/else if/else chain in Swift? Is there a case where each approach is better? Please explain the concept before showing any code — I want to think it through first.
    Build a Practice Example Generate a heavily commented example to study
    Write a Swift if/else if/else example based on a simple realistic app feature — like showing a message based on a user’s loyalty points, or applying a discount based on a cart total. Show all three forms (if only, if/else, and if/else if/else) somewhere in the example. Add a comment on every single line explaining what it does and why, written for a complete beginner.
    2.3
    Logical Operators (&& || !)
    ⏱ 25 min Control Flow

    So far you’ve been checking one condition at a time. But real decisions usually involve more than one thing. “You can watch this movie if you’re 18 or older AND you have a subscription.” “You get a discount if it’s your birthday OR you’re a member.” These are multiple conditions combined into one decision.

    Swift gives you three logical operators for exactly this. && means AND, || means OR, and ! means NOT. They let you combine or flip Bool values so you can express complex conditions without nesting a pile of if statements inside each other.

    The good news is that these operators work exactly like the English words “and”, “or”, and “not”. Once you make that connection, they become very natural to read and write.

    let age = 20                           // The user's age
    let hasSubscription = true             // Whether they have an active subscription
    let isBirthday = false                 // Whether today is their birthday
    
    // AND: both conditions must be true for the whole thing to be true
    if age >= 18 && hasSubscription {
        print("Enjoy the content!")          // Runs — both conditions are true
    }
    
    // OR: at least one condition must be true
    if hasSubscription || isBirthday {
        print("You get a bonus reward!")     // Runs — hasSubscription is true (birthday doesn't matter)
    }
    
    // NOT: flip a Bool from true to false, or false to true
    if !isBirthday {
        print("No birthday bonus today.")     // Runs — !false is true
    }
    Output
    Enjoy the content!
    You get a bonus reward!
    No birthday bonus today.
    OperatorWhat it means
    && (AND)Both sides must be true for the result to be true. If either side is false, the whole thing is false.
    || (OR)At least one side must be true. The whole thing is only false when both sides are false.
    ! (NOT)Flips a Bool. !true becomes false. !false becomes true. Place it directly in front of a Bool, no space.
    && AND — every condition must pass
    let hasID = true
    let isOnGuestList = false
    
    if hasID && isOnGuestList {           // Both must be true — only hasID is true
        print("Welcome in!")
    } else {
        print("Sorry, you need both.")      // This runs — isOnGuestList is false
    }
    With &&, one false value is enough to make the entire condition false. This replaces the nested if pattern from Lesson 2.2 — cleaner and reads more like natural English.
    || OR — any one condition being true is enough
    let isStudent = false
    let isSenior = true
    
    if isStudent || isSenior {            // At least one must be true — isSenior is
        print("Discounted ticket!")         // This runs
    } else {
        print("Full price.")
    }
    With ||, the condition only fails when every single part is false. Even if ten things are false, one true one makes the whole condition true.
    ! NOT — flip the Bool value
    var isGameOver = false
    
    if !isGameOver {                      // Read aloud: "if NOT isGameOver"
        print("Keep playing!")             // !false is true — this runs
    }
    Put ! directly in front of a Bool with no space: !isGameOver not ! isGameOver. A space causes a compiler error. Often cleaner than writing isGameOver == false.
    combined Mix operators with parentheses for complex logic
    let isWeekend = true
    let hasChores = false
    let friendsAvailable = true
    
    // Can hang out if: it's the weekend AND (no chores OR friends are free)
    if isWeekend && (!hasChores || friendsAvailable) {
        print("Let's go!")                 // Runs — all conditions satisfied
    }
    Use parentheses to group conditions when mixing operators. The parenthesised part is evaluated first, just like in maths. Without parentheses, && has higher priority than || — parentheses make your intent explicit and your code easier to read.
    No space after !: Write !isGameOver, not ! isGameOver. The ! must be attached directly to the value it’s flipping. A space between them will produce a compiler error.
    Readability first: If a condition is getting long and hard to follow, pull it apart into named Bool constants. let canEnter = isOldEnough && hasTicket — then just write if canEnter. Same logic, much easier to understand at a glance.
    🏆 Challenge
    Theme Park Ride Check

    A theme park ride has two rules: riders must be at least 120cm tall AND must not have a fear of heights. Create variables for heightCm (an Int) and fearOfHeights (a Bool). Write an if/else that prints "Enjoy the ride!" when both requirements are met. If not, print a specific message explaining which requirement wasn’t met. Try to give a different message for each failure scenario.

    Hint: For the “can ride” check you’ll combine && and ! together. For failure messages, think about separate else if branches checking each condition individually.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    Explain Swift’s &&, ||, and ! operators using a non-coding analogy — something like getting into a theme park, applying for a loan, or qualifying for a discount. Then ask me five true/false questions where I work out the result of combining two Bools. Ask one at a time and correct me if I get one wrong.
    Can you explain “short-circuit evaluation” in Swift? I’ve heard that Swift sometimes doesn’t bother checking the second condition at all. I want to understand why that happens and when it matters — please explain the concept in plain English before any code examples.
    Build a Practice Example Generate a heavily commented example to study
    Write a Swift example that uses &&, ||, and ! together in a realistic app scenario — like checking whether a user can access premium content or whether a purchase should get a discount. Use all three operators at least once. Add a comment on every single line explaining the logic, written for a beginner who is seeing these operators for the first time.
    2.4
    switch Statements
    ⏱ 30 min Control Flow

    You’ve seen how if / else if / else handles multiple conditions. But when you’re checking one specific value against a list of possible options, a switch statement is often a much cleaner choice.

    Think of a vending machine. You press A1 and get chips. You press B2 and get a drink. You press C3 and get chocolate. The machine doesn’t work through a long chain of “was it A1? No. Was it B2? No. Was it C3?” — it just checks the one value you gave it and jumps straight to the matching slot. That’s exactly what a switch statement does.

    Swift’s switch statements are also more powerful than in many other languages. They can match ranges of numbers, multiple values at once, and they require you to handle every possible case — which catches bugs before they happen.

    let day = "Monday"                   // The value we want to check
    
    switch day {                           // Start the switch with the value to examine
    case "Monday":
        print("Back to the grind.")        // "Monday" matches — this runs
    case "Friday":
        print("Almost the weekend!")
    case "Saturday", "Sunday":          // Two values can share one case
        print("Enjoy your weekend!")
    default:                               // Handles any value not listed above
        print("Just another weekday.")
    }
    Output
    Back to the grind.
    PartWhat it does
    switch dayThe value being examined. Swift compares it against each case below in order.
    case "Monday":If day equals “Monday”, this block runs. No curly braces — the next case or default ends this block automatically.
    case "Saturday", "Sunday":Separate multiple matching values with commas to give them the same response.
    default:The catch-all, like else. Runs when no case matched. Required unless every possible value is already covered.
    Important difference from other languages: Swift switch cases do NOT fall through to the next case. In languages like C or JavaScript you need a break statement to stop execution. Swift ends each case automatically. Fewer accidental bugs, less code to write.

    More Powerful Ways to Use switch

    range matching Match a whole range of numbers with one case
    let score = 83
    
    switch score {
    case 90...100:                        // ... means "90 up to and including 100"
        print("Grade: A")
    case 80...89:
        print("Grade: B")                   // 83 falls here — this runs
    case 70...79:
        print("Grade: C")
    default:
        print("Grade: F")
    }
    The ... creates a closed range that includes both endpoints. This is where switch shines over a long if / else if chain — ranges make the intent much clearer at a glance.
    multiple values per case Group values that produce the same result
    let month = 2                         // February
    
    switch month {
    case 1, 2, 3:                        // January, February, or March
        print("Winter / Early Spring")      // February is 2 — this runs
    case 4, 5, 6:
        print("Spring / Early Summer")
    case 7, 8, 9:
        print("Summer / Early Autumn")
    case 10, 11, 12:
        print("Autumn / Winter")
    default:
        print("Invalid month number")
    }
    Separate multiple matching values with commas. No need to write the same print statement four times — one case handles them all.
    where clause Add an extra condition to a case
    let temperature = -5
    
    switch temperature {
    case let t where t < 0:             // Match any negative number, name it "t"
        print("Freezing: \(t) degrees")    // Runs — -5 is less than 0. "t" holds -5
    case let t where t > 30:
        print("Very hot: \(t) degrees")
    default:
        print("Temperature: \(temperature)")
    }
    The where keyword adds a filter to a case. let t gives the matched value a temporary name you can use inside that case. This is an intermediate pattern — you’ll see it more as you build real apps.
    switch vs if — when to use which: Reach for switch when you’re checking one value against a list of specific options. Reach for if when conditions involve different variables or more complex expressions. Either can work — pick whichever reads more clearly.
    🏆 Challenge
    Planet Gravity Facts

    Create a constant called planet and set it to the name of any planet in our solar system as a String. Write a switch statement that prints a gravity fact for at least five planets — something like "Mars: 0.38x Earth gravity". Include a default case that prints "Unknown planet". Group any two planets with similar gravity into a single case using a comma.

    Hint: Each case matches a String. You can list two values in one case with a comma: case "Mercury", "Mars":.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    Explain Swift switch statements to me like I’m a beginner. I specifically want to understand two things: how they differ from an if/else if chain, and what “exhaustive” means in this context — why does Swift require every possible value to be handled? Use a real-world analogy before showing any syntax.
    Show me a Swift switch statement that has a bug in it — either a missing case, wrong syntax, or a logic error — and ask me to spot what’s wrong and explain why. Start simple and increase the difficulty if I get it right.
    Build a Practice Example Generate a heavily commented example to study
    Write a Swift switch statement that demonstrates at least three features: matching exact values, matching a number range with …, and a default case. Build it around a realistic scenario like a restaurant menu, a game difficulty level, or a delivery status tracker. Comment every single line for a complete beginner, explaining what each part does and why.
    2.5
    The Ternary Operator
    ⏱ 20 min Swift Basics

    You’ve been writing full if/else blocks to make decisions. Now meet the compact version: the ternary operator. It lets you express a simple two-option decision in a single line of code.

    Think of it like a question with exactly two possible answers. “Is it raining? If yes — umbrella. If no — no umbrella.” The ternary operator is that structure compressed into one line: condition ? valueIfTrue : valueIfFalse. Ask a question, give the yes answer, give the no answer.

    The word “ternary” just means it takes three parts: the question, the yes answer, and the no answer. Don’t let the name throw you off. Once you see the pattern a couple of times, it becomes second nature.

    let temperature = 28                  // Current temperature in Celsius
    
    // The long way with if/else:
    var advice: String
    if temperature > 25 {
        advice = "Wear sunscreen!"
    } else {
        advice = "Bring a jacket."
    }
    print(advice)                          // "Wear sunscreen!"
    
    // The short way with the ternary operator — same result, one line:
    let quickAdvice = temperature > 25 ? "Wear sunscreen!" : "Bring a jacket."
    print(quickAdvice)                     // "Wear sunscreen!"
    Output
    Wear sunscreen!
    Wear sunscreen!
    PartWhat it does
    temperature > 25The condition. Evaluated first — produces true or false.
    ?Separates the condition from the two possible values. Read it aloud as “then”.
    "Wear sunscreen!"The value produced when the condition is true. Comes immediately after the ?.
    :Separates the true value from the false value. Read it aloud as “otherwise”.
    "Bring a jacket."The value produced when the condition is false.
    How to read a ternary aloud: Treat it as a question. temperature > 25 ? "Hot" : "Cold" reads as: “Is the temperature greater than 25? If yes — ‘Hot’. If no — ‘Cold’.” That mental model will carry you far.

    Common Ways to Use the Ternary Operator

    assign a constant The most common use — set a let constant based on a condition
    let isLoggedIn = true
    let greeting = isLoggedIn ? "Welcome back!" : "Please sign in."
    print(greeting)   // "Welcome back!"
    The ternary shines when assigning a let constant. You can’t assign a constant inside an if/else block easily — the ternary handles it cleanly in one expression, keeping the result immutable.
    inside string interpolation Embed a conditional value directly inside a string
    let itemCount = 1
    // Ternary inside \( ) picks the right word based on the count
    print("You have \(itemCount) \(itemCount == 1 ? "item" : "items") in your cart.")
    // "You have 1 item in your cart."  (not "1 items")
    A classic real-world use: getting singular and plural right. “1 item” vs “2 items”. A ternary inside string interpolation handles it in one expression — no extra variables needed.
    with numbers Produce a different number based on a condition
    let isMember = false
    let price = 10.00
    let finalPrice = isMember ? price * 0.8 : price   // 20% off for members
    print("Total: $\(finalPrice)")   // "Total: $10.0" — not a member, full price
    Both sides of the ternary must produce the same type. You can’t return a String on one side and an Int on the other — Swift won’t allow it.
    nested ternary — avoid A ternary inside a ternary: technically works, but please don’t
    // This works but is a nightmare to read:
    let score = 75
    let grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F"
    print(grade)   // "C" — correct, but good luck reading that a month from now
    
    // Use switch or if/else if instead — same result, much clearer
    Nested ternaries get very hard to follow. Whenever you need more than two outcomes, reach for switch or if / else if. Readable code always beats clever code.
    When NOT to use the ternary: The ternary is great for simple, single-line decisions where you’re choosing between exactly two values. If the logic is complicated, has more than two outcomes, or needs multiple lines to execute, use a regular if/else. Squeezing complex logic into a ternary makes your code harder for everyone — including future you — to understand.
    🏆 Challenge
    Pass or Fail

    Create a constant called testScore (an Int out of 100). Using the ternary operator, create a constant called result that equals "Pass" if the score is 50 or above and "Fail" if it’s below 50. Then write a single print call that outputs something like "Score: 72 — Result: Pass" using string interpolation. The whole solution should be exactly three lines of code.

    Hint: You can use \(result) inside your print string. Which comparison operator means “50 or above”?
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    Explain the Swift ternary operator to me in plain English — what each of the three parts means and how to read one aloud. Then give me five ternary expressions with the results hidden and ask me what value each one produces. No code examples from you yet — just the quiz, one question at a time.
    When should I use a ternary operator instead of a regular if/else in Swift? What makes ternary the cleaner choice in some situations, and what makes it worse in others? I want practical guidelines I can actually apply when writing code — not just syntax rules.
    Build a Practice Example Generate a heavily commented example to study
    Write four short Swift examples that each use the ternary operator in a different context: assigning a String, assigning a number, embedding one inside string interpolation, and using it with a Bool condition. Add a comment on every line explaining what the condition checks and which value gets chosen. Write all the comments for a beginner who has never seen the ternary operator before.

    Stage 2 Recap: Making Decisions

    You just covered five of the most fundamental ideas in all of programming. Every single app in existence uses these tools. Here’s what you now have in your toolkit:

    • Lesson 2.1 — Comparison operators: Six operators (==, !=, <, >, <=, >=) that compare two values and produce a Bool. Every decision in your app starts here.
    • Lesson 2.2 — if / else if / else: The core decision-making tool. Swift checks conditions top to bottom and stops at the first match. Exactly one branch runs.
    • Lesson 2.3 — Logical operators: && (AND), || (OR), and ! (NOT) let you combine and flip Bool values so you can check multiple things at once without nesting if statements inside each other.
    • Lesson 2.4 — switch statements: When you’re checking one value against many specific options, switch is cleaner than a chain of else-ifs. Supports exact values, comma-separated values, and number ranges — and forces you to handle every possible case.
    • Lesson 2.5 — The ternary operator: A compact one-line way to express a simple two-option decision. Best for assigning values and embedding choices inside string interpolation. Use it when the logic is simple — reach for if/else when it isn’t.

    If you haven’t done all five challenges yet, go back and do them now. Reading code builds familiarity. Writing code builds understanding. Those are two different things, and you need both.

    In Stage 3, you’ll learn about loops — how to make Swift repeat a block of code automatically so you’re not writing the same thing over and over. You’ll also meet functions, which let you give a block of code a name and reuse it anywhere in your app. With decisions and loops both in your toolkit, the things you can build are about to grow fast.

    Learn Swift Stage 3: Repeating Things

    Loops are how you tell Swift to do something more than once — and they’re about to become one of the most useful tools you have.

    To follow along, open a Swift Playground in Xcode. Each lesson takes about 25-30 minutes including the challenge at the end. You should have completed Stages 1 and 2 before starting here — specifically, you should already be comfortable with variables, data types, and if/else statements.

    By the end of Stage 3 you will understand how to repeat code using for loops and while loops, how to control that repetition with break and continue, and how to use nested loops — plus when not to.

    03
    Stage 3
    Repeating Things
    5 lessons · ~2.5 hrs
    3.1
    For Loops with Ranges
    ⏱ 20 min Swift Loops

    Imagine you had to write print("Hello!") ten times in a row. You could do it — but it would be tedious and silly. That’s exactly the kind of problem a loop solves.

    A for loop runs a block of code a set number of times. Think of it like a lap counter at a running track. You decide how many laps, the runner goes, and the counter keeps track so you don’t have to.

    In Swift, you tell a for loop how many times to run by giving it a range — a starting number and an ending number. Here’s the simplest version:

    for i in 1...5 {          // run this loop for i = 1, 2, 3, 4, 5
        print("Lap \(i)")      // print the current lap number using string interpolation
    }
    Output
    Lap 1
    Lap 2
    Lap 3
    Lap 4
    Lap 5
    PartWhat it does
    forThe keyword that starts a for loop. Swift sees this and knows repetition is coming.
    iA temporary variable that holds the current count. It automatically gets a new value each time the loop runs. You can name this anything — number, count, lap — whatever makes sense.
    inConnects the variable to the range. Read the whole thing as: “for each value of i, in the range 1 to 5…”
    1...5The closed range operator. It means “every number from 1 up to and including 5.” The three dots are the range operator.
    { }The loop body. Everything inside these curly braces runs once for each number in the range.
    print("Lap \(i)")Uses string interpolation to print the current value of i. On the first loop i is 1, then 2, and so on.
    Common mistake: Beginners sometimes write for i in 1...5 and then try to use i after the closing brace. That won’t work — i only exists inside the loop. Once the loop ends, it disappears.

    The Two Range Operators

    Swift has two ways to write a range, and they differ in one important way: whether the last number is included or not.

    1…10 Closed range — includes the last number
    for i in 1...10 {
        print(i)    // prints 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
    }
    The three-dot operator (...) creates a closed range. The loop runs for every number from 1 up to and including 10. Use this when you want to include the final value.
    1..<10 Half-open range — excludes the last number
    for i in 1..<10 {
        print(i)    // prints 1, 2, 3, 4, 5, 6, 7, 8, 9 — stops before 10
    }
    The dot-dot-less-than operator (..<) creates a half-open range. The loop runs up to but not including the last number. This is extremely common in iOS development because lists start at index 0.
    _ in range When you don’t need the loop variable
    for _ in 1...3 {
        print("Hello!")    // prints "Hello!" exactly 3 times
    }
    If you just want to run a block of code a set number of times and you don’t care about the current count, replace the variable name with an underscore _. Swift treats this as “I know there’s a value here, but I don’t need it.”
    stride(from:to:by:) Count by custom steps — skip numbers or count down
    // Count by 2s: 0, 2, 4, 6, 8, 10
    for i in stride(from: 0, to: 11, by: 2) {
        print(i)
    }
    
    // Count down: 10, 8, 6, 4, 2, 0
    for i in stride(from: 10, through: 0, by: -2) {
        print(i)
    }
    When you need to count by something other than 1, use stride. Use to: (excludes the end) or through: (includes the end). Use a negative by: value to count downward.
    Good to know: The name i is a programming convention short for “index” or “iteration.” You’ll see it everywhere. But it’s just a regular variable name — feel free to use something more descriptive like day or score when it makes your code easier to read.
    Challenge 3.1 Multiplication Table

    Write a for loop that prints the 7 times table from 7 x 1 all the way up to 7 x 12. Each line should look like this: 7 x 1 = 7

    Then try a bonus: use a stride loop to print only the even multiples of 7 (7 x 2, 7 x 4, 7 x 6, and so on up to 7 x 12).

    Hint: Use string interpolation inside your print statement. You can do math directly inside \( ) — for example \(7 * i) gives you the result of 7 times i.

    AI Practice Prompts

    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain the difference between the closed range operator (1…10) and the half-open range operator (1..<10) in Swift. Give me a real-world analogy for each one that doesn’t involve programming. Then ask me a question to check my understanding.
    I’m learning Swift for loops and I want to make sure I really understand them. Without writing any code for me, can you ask me 3 questions about for loops — one at a time — and tell me if I get each one right or wrong before moving on?
    Build a Practice Example Study commented code to reinforce what you’ve learned
    Write a Swift for loop example that counts down from 10 to 1 and then prints “Blastoff!” at the end. Add a comment on every single line explaining what it does and why — write the comments for a complete beginner who has never seen a loop before.
    Give me 3 short Swift for loop examples that each use a different range or stride pattern. Make each one slightly more interesting than the last. Add inline comments throughout so I understand the logic without needing any other explanation.
    3.2
    For Loops Over Arrays
    ⏱ 20 min Swift Basics

    In the last lesson you looped over a range of numbers. But loops get really powerful when you loop over a list of actual things — names, scores, colors, whatever your app is working with.

    That list is called an array. Think of an array like a numbered shelf with slots. Each slot holds one value, and you can have as many slots as you want. We’ll go deep on arrays in Stage 4 — but right now you just need to know enough to loop over one.

    // Create an array of names — square brackets hold the list
    let names = ["Alice", "Bob", "Charlie"]
    
    // Loop over every name in the array
    for name in names {
        print("Hello, \(name)!")    // name holds each value in turn
    }
    Output
    Hello, Alice!
    Hello, Bob!
    Hello, Charlie!
    PartWhat it does
    let names = [...]Creates an array called names that holds three strings. The values are separated by commas inside square brackets. We use let because we’re not changing the array here.
    for name in namesStarts a loop. On each pass, the variable name is automatically set to the next item in the array — first “Alice”, then “Bob”, then “Charlie”.
    name (the loop variable)This is a new temporary variable you’re creating. It’s a singular version of names, but you can call it anything. Swift assigns it the current array item automatically.
    print("Hello, \(name)!")Uses string interpolation to include the current name in the output. Each time through the loop, name holds a different value.
    Naming convention: When you have an array called names, name the loop variable name (singular). This makes your code read almost like plain English: “for each name in names.” Same idea: for score in scores, for color in colors.

    More Array Loop Patterns

    Loop over numbers Arrays can hold any type, not just strings
    let scores = [95, 82, 74, 100, 60]
    
    for score in scores {
        if score >= 90 {
            print("\(score) — Great job!")
        } else {
            print("\(score) — Keep practicing.")
        }
    }
    Here the array holds integers instead of strings. Inside the loop you can use the value score in any expression — including an if statement. This is a preview of how real apps process lists of data.
    enumerated() Get both the position and the value at the same time
    let fruits = ["Apple", "Banana", "Cherry"]
    
    for (index, fruit) in fruits.enumerated() {
        print("\(index + 1). \(fruit)")    // index starts at 0, so we add 1
    }
    Calling .enumerated() gives you both the position (starting from 0) and the value on each loop pass. The (index, fruit) syntax unpacks both values at once — you’ll see this pattern in real iOS apps all the time. Output: 1. Apple, 2. Banana, 3. Cherry.
    Loop to build a result Use a variable outside the loop to accumulate values
    let prices = [4.99, 12.50, 3.25, 7.00]
    var total = 0.0          // starts at zero — declared with var so we can change it
    
    for price in prices {
        total += price        // add each price to the running total
    }
    
    print("Total: \(total)")  // 27.74
    Notice total is declared with var outside the loop so its value persists between passes. total += price is shorthand for total = total + price. This pattern — loop to accumulate — is one of the most common things you’ll do with arrays.
    Important: If you need to change a variable inside a loop, it must be declared with var, not let. The array itself can be let if you’re not changing it — only the accumulator needs var.
    Challenge 3.2 Team Report Card

    Create an array of 5 player names and a separate array of 5 scores (one for each player). Loop through the names and for each one print a message like: Alice scored 87 points.

    Bonus: After the loop, calculate and print the average score across all players.

    Hint: To get a matching score for each name you can loop over a range (0..<names.count) and use the loop variable as an index into both arrays. names.count gives you the number of items in the array.

    AI Practice Prompts

    Deepen Your Understanding Clarify concepts without relying on generated code
    I just learned how to loop over an array in Swift. Explain to me why you would use “for item in array” instead of “for i in 0…array.count” to loop over items. What are the tradeoffs? Use plain English before showing any code.
    In Swift, I see that loop variables like “name” in “for name in names” only exist inside the loop body. Can you explain why that is — what concept does that relate to? Don’t show me code yet, just explain the idea.
    Build a Practice Example Study commented code to reinforce what you’ve learned
    Write a Swift example that loops over an array of temperatures in Celsius and prints each one converted to Fahrenheit. Put a comment on every line explaining what it does and why — write for a beginner who is seeing Swift arrays and loops for the first time.
    Show me how to use enumerated() with a Swift array in 2 different situations where knowing the index actually matters. Add heavy inline comments so I understand exactly what enumerated() is doing and why I’d use it over a basic loop.
    3.3
    While Loops
    ⏱ 20 min Swift Basics

    A for loop is great when you know in advance how many times you want to repeat something. But what if you don’t know? What if you want to keep going until something changes?

    That’s the job of a while loop. Think of it like a vending machine. It keeps taking coins as long as the balance is less than the item price. It doesn’t know upfront how many coins you’ll put in — it just checks each time and stops when the condition is met.

    var coins = 0               // start with 0 coins
    let price = 5               // the item costs 5 coins
    
    while coins < price {         // keep looping as long as we don't have enough
        coins += 1               // add one coin each time
        print("Coins inserted: \(coins)")
    }
    
    print("Vending!")            // this runs after the loop ends
    Output
    Coins inserted: 1
    Coins inserted: 2
    Coins inserted: 3
    Coins inserted: 4
    Coins inserted: 5
    Vending!
    PartWhat it does
    whileThe keyword that starts a while loop. Swift will keep repeating the loop body as long as the condition is true.
    coins < priceThe condition. Swift checks this before every single loop pass. The moment it becomes false (coins equals 5), the loop stops immediately.
    coins += 1This is what changes the condition. Every pass adds 1 to coins, so eventually the condition will become false. Without this line, you’d have an infinite loop.
    Code after the closing }Runs once after the loop ends. The loop must finish before Swift moves on to the next statement.
    Danger zone — infinite loops: If the condition in a while loop never becomes false, the loop runs forever and your app freezes. This is called an infinite loop. Always make sure something inside your loop eventually changes the condition. In the example above, coins += 1 is the thing that eventually makes coins < price false.

    While Loop Variations

    repeat-while Always runs at least once before checking the condition
    var attempts = 0
    
    repeat {
        attempts += 1
        print("Attempt \(attempts)")
    } while attempts < 3          // condition is checked AFTER each run
    A repeat-while loop is like a while loop but the condition is checked at the bottom instead of the top. This guarantees the loop body runs at least once, even if the condition starts out false. Use this when you always want at least one attempt — like showing a dialog box or making a first network request.
    while true Run forever until you explicitly break out
    var number = 1
    
    while true {                  // condition is always true
        print(number)
        number *= 2               // double the number each time
        if number > 100 {
            break                   // stop when number exceeds 100
        }
    }
    while true creates a loop with no natural end condition — it will run until something inside explicitly uses break to stop it. You’ll learn about break in the next lesson. Use this pattern when the stopping logic is complex enough that it’s cleaner to put it inside the loop.
    while with Bool variable Use a flag to control the loop from anywhere inside it
    var isRunning = true         // this Bool controls the loop
    var count = 0
    
    while isRunning {
        count += 1
        print("Count: \(count)")
        if count == 4 {
            isRunning = false      // flip the flag to stop the loop
        }
    }
    Using a Bool “flag” variable as the condition gives you clear, readable control. When the situation that should stop the loop occurs, flip the flag to false. This is especially useful when the stop condition might occur in different places within the loop body.
    When to use while vs for: Use a for loop when you know the exact count or have a collection to go through. Use a while loop when the stopping point depends on something that changes while the loop runs — like user input, a game state, or a network response arriving.
    Challenge 3.3 Savings Goal

    You want to save up $500. You start with $0 and add $75 each week. Write a while loop that prints your savings balance at the end of each week, and stops once you’ve reached or exceeded $500. At the end, also print how many weeks it took.

    Bonus: Try the same thing using a repeat-while loop and see if the output is any different.

    Hint: You’ll need two variables — one for the current balance and one to count the weeks. The balance variable needs var because it changes. Think about whether your condition should be balance < 500 or balance <= 500 and what difference it makes.

    AI Practice Prompts

    Deepen Your Understanding Build mental models without jumping to code
    Explain the difference between a while loop and a repeat-while loop in Swift using a real-world analogy — something that doesn’t involve code. Then tell me a specific situation where you’d choose repeat-while over while and explain why.
    I want to understand what makes a while loop different from a for loop at a conceptual level — not just syntactically. When would a experienced developer reach for a while loop instead of a for loop? Give me 3 scenarios in plain English before showing any code.
    Build a Practice Example Generate heavily commented code you can learn from
    Write a Swift while loop that simulates a number guessing game where the “player” keeps guessing randomly until they guess the right number. Use hard-coded values, not real random numbers — just make it look like a simulation. Add a comment on every single line for a beginner.
    Give me 2 while loop examples in Swift — one that could easily be rewritten as a for loop, and one where a while loop is clearly the better choice. Add inline comments explaining why each choice was made.
    3.4
    Break and Continue
    ⏱ 20 min Swift Basics

    Loops don’t always need to run perfectly from start to finish. Sometimes you hit a situation where you want to stop the whole loop early. Other times you want to skip just one pass but keep going. Swift gives you two keywords for this: break and continue.

    Think of a conveyor belt at a factory. continue is like saying “skip this defective item, keep the belt running.” break is like hitting the emergency stop — the whole belt shuts off immediately.

    // break: stop the loop the moment we find what we need
    let items = ["pen", "notebook", "scissors", "tape", "stapler"]
    let target = "scissors"
    
    for item in items {
        print("Checking: \(item)")
        if item == target {
            print("Found it!")
            break            // stop the entire loop right here
        }
    }
    Output
    Checking: pen
    Checking: notebook
    Checking: scissors
    Found it!

    Notice the loop never checked “tape” or “stapler” — break stopped everything the moment we found what we were looking for.

    Now here’s continue — it skips the rest of the current pass and jumps to the next one:

    // continue: skip negative numbers, process only positives
    let numbers = [3, -1, 7, -5, 2, 9]
    
    for number in numbers {
        if number < 0 {
            continue         // skip this item and move to the next one
        }
        print("Positive: \(number)")
    }
    Output
    Positive: 3
    Positive: 7
    Positive: 2
    Positive: 9
    KeywordWhat it does
    breakImmediately exits the entire loop. Code after the closing brace of the loop continues to run normally, but the loop itself is done — no more passes happen.
    continueSkips only the rest of the current loop pass. The loop itself keeps running — it just jumps directly to the next item or next count, skipping any code below the continue statement.

    More Patterns with Break and Continue

    break in while Exit a while loop when a condition inside is met
    var energy = 100
    
    while true {
        energy -= 15
        print("Energy remaining: \(energy)")
        if energy <= 0 {
            print("Out of energy!")
            break            // exits the while true loop
        }
    }
    break works in while loops too. When combined with while true, it’s the only way to stop the loop. This is a clean pattern when the stopping logic is complex — put the exit condition wherever it makes most sense in the loop body.
    continue to filter Use continue to process only items that match a condition
    let words = ["swift", "xcode", "apple", "ios", "swiftui"]
    
    for word in words {
        if !word.hasPrefix("swift") {    // if the word doesn't start with "swift"
            continue                         // skip it
        }
        print("Swift-related: \(word)")
    }
    continue is great for filtering — skip items you’re not interested in and only process the ones that matter. This pattern appears a lot in real apps when you’re processing a list and only some items need handling.
    labeled break Break out of a specific outer loop from inside a nested one
    outerLoop: for i in 1...3 {
        for j in 1...3 {
            if i == 2 && j == 2 {
                print("Stopping everything at (\(i), \(j))")
                break outerLoop     // exits the outer loop, not just the inner one
            }
            print("(\(i), \(j))")
        }
    }
    Without a label, break only exits the innermost loop. Add a label (a name followed by a colon) to the outer loop and use break labelName to exit that specific loop. This is less common but useful in nested loop situations you’ll see in the next lesson.
    Common confusion: break exits the loop, continue skips one pass. A very common mistake is using break when you meant continue, which stops the loop entirely when you only wanted to skip one item. Re-read the output carefully when something looks wrong.
    Challenge 3.4 Quality Control

    You have an array of product weights: [102, 98, 115, 50, 103, 99, 110, 30, 105]. Acceptable weight is between 90 and 115 (inclusive). Write a loop that:

    • Skips any product that is within the acceptable range using continue
    • Prints a “REJECT” message for any out-of-range product
    • Stops the entire production line (exits the loop) with a “HALT” message if you encounter a weight under 60 (catastrophic defect)
    Hint: Check the catastrophic condition first with an if statement and break. Then check if the weight is acceptable and use continue. Anything still running after those checks is a normal rejection.

    AI Practice Prompts

    Deepen Your Understanding Test your understanding through dialogue, not code
    I just learned about break and continue in Swift loops. Can you give me 4 short scenarios (described in plain English, no code) and ask me to decide for each one whether break, continue, or neither would be the right tool? Tell me if I’m right or wrong and explain why.
    In Swift, what happens exactly when break is hit inside a nested loop? Does it exit all loops or just the inner one? And what about continue — does it skip to the next pass of the inner loop or the outer one? Explain with a clear example before showing any code.
    Build a Practice Example Generate examples rich enough to learn from
    Write a Swift example that loops over a list of numbers and uses both break and continue — break when a specific condition is met, continue to skip certain values along the way. Add a comment on every line for a beginner, and include a comment explaining exactly when and why execution jumps on the break and continue lines.
    Show me a real-world-inspired Swift example where using a labeled break is genuinely the cleanest solution — not a situation where I could just use a regular break. Add comments that explain why the label was needed and what would happen without it.
    3.5
    Nested Loops
    ⏱ 20 min Swift Basics

    A nested loop is simply a loop inside another loop. The outer loop runs once, and for each pass of the outer loop the entire inner loop runs from start to finish. Then the outer loop goes to its next pass, and the inner loop runs all the way through again.

    Think of a clock. The minute hand (outer loop) moves forward once per hour. Each time it does, the second hand (inner loop) completes a full 60-second cycle. For every 1 step of the outer loop, the inner loop takes 60 steps.

    // Print a simple multiplication grid (3x3)
    for row in 1...3 {                            // outer loop: rows
        for col in 1...3 {                        // inner loop: columns
            print("\(row) x \(col) = \(row * col)")
        }
        print("---")                            // runs after each full inner loop
    }
    Output
    1 x 1 = 1
    1 x 2 = 2
    1 x 3 = 3
    2 x 1 = 2
    2 x 2 = 4
    2 x 3 = 6
    3 x 1 = 3
    3 x 2 = 6
    3 x 3 = 9
    PartWhat it does
    for row in 1...3The outer loop. It runs 3 times — for row = 1, row = 2, and row = 3.
    for col in 1...3The inner loop. For each value of row, this entire loop runs 3 times — for col = 1, 2, and 3.
    Total iterationsThe outer loop runs 3 times. The inner loop runs 3 times per outer pass. Total: 3 × 3 = 9 iterations. With nested loops, the total work multiplies quickly.
    print("---") (after inner loop)This line is inside the outer loop but outside the inner one. It only runs once per outer pass — after the inner loop completes for that row.

    Practical Nested Loop Patterns

    Loop over a grid Combine row and column to address every cell
    // Simulate checking every seat in a cinema (5 rows, 8 seats each)
    for row in 1...5 {
        for seat in 1...8 {
            print("Row \(row), Seat \(seat)")
        }
    }
    // Total: 40 iterations (5 x 8)
    Nested loops are a natural fit for any grid-based data — board games, spreadsheets, seating charts, pixel grids. Each coordinate is uniquely identified by combining the outer and inner loop variables.
    Nested loop with break Search a grid and stop when something is found
    let grid = [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]
    ]
    
    searchLoop: for row in grid {
        for value in row {
            if value == 5 {
                print("Found 5!")
                break searchLoop   // exit both loops at once
            }
        }
    }
    This is a two-dimensional array (an array of arrays). The outer loop gives you each row, the inner loop gives you each value within that row. The labeled break searchLoop exits both loops at once — without the label, only the inner loop would break.
    Performance warning: Every layer of nesting multiplies the total number of iterations. A loop of 100 inside a loop of 100 runs 10,000 times. Add a third loop of 100 and you’re at 1,000,000 iterations. This can make your app feel slow or freeze. If you find yourself writing three or more nested loops, that’s a strong signal to step back and think about a different approach.

    When Not to Use Nested Loops

    Nested loops are a legitimate tool, but they’re also one of the easiest ways to accidentally write slow code. Here are the situations to watch out for:

    Pattern to avoid Don’t nest unnecessarily — sometimes one loop is enough
    // Unnecessary nesting — this just adds complexity for no reason
    let names = ["Alice", "Bob", "Charlie"]
    let greetings = ["Hi", "Hello", "Hey"]
    
    // Bad: this prints every greeting for every name (9 outputs)
    for name in names {
        for greeting in greetings {
            print("\(greeting), \(name)!")
        }
    }
    
    // Better: if you want one greeting per person, just use one loop
    for name in names {
        print("Hi, \(name)!")
    }
    Before nesting a loop, ask yourself: “Do I actually need every combination of outer and inner values?” If the answer is no — if you just want to process each item once — a single loop is almost always cleaner. Only reach for nesting when you genuinely need to process combinations or grid-style data.
    Rule of thumb: If your nested loop is getting hard to read or feels like it’s doing more work than it should, that’s a sign to pause. In Stage 4 you’ll learn about higher-order functions like map, filter, and forEach that often replace nested loops with cleaner, faster alternatives.
    Challenge 3.5 Star Pattern

    Use nested loops to print a right-triangle pattern of stars. For a size of 5, the output should look like this:

    *
    **
    ***
    ****
    *****

    Bonus: Modify it so the triangle is inverted (5 stars on top, 1 on the bottom). Then try making a solid square (5 stars wide, 5 rows tall).

    Hint: The outer loop controls which row you’re on. The inner loop prints stars for that row. On row 1 you print 1 star, on row 2 you print 2 stars — so the inner loop’s range depends on the outer loop variable. You can build a string by starting with an empty string and appending to it inside the inner loop, then print after the inner loop ends.

    AI Practice Prompts

    Deepen Your Understanding Build intuition for when nested loops make sense
    I just learned about nested loops in Swift. Without writing any code, can you explain why nesting a loop of 50 inside a loop of 50 might cause problems that a single loop of 100 wouldn’t? Use a real-world analogy. Then ask me if I understand before moving on.
    Give me 3 real-world scenarios in plain English — no code — where nested loops would be the natural choice. Then give me 2 scenarios where it might seem like you need nested loops but a single loop would actually work better. Explain why for each one.
    Build a Practice Example Study a nested loop example with thorough commentary
    Write a Swift nested loop example that searches a 2D grid (an array of arrays) for a specific value and prints its row and column when found. Use a labeled break to exit both loops at once. Add a comment on every single line — write the comments for a beginner who has just finished learning basic loops.
    Show me a Swift example that uses nested loops to process some kind of grid-based data — something you’d genuinely encounter in an iOS app, like checking a game board or processing image pixels conceptually. Add heavy inline comments so I understand both what the code does and why nested loops were the right choice here.

    Stage 3 Recap: What You’ve Learned

    You now know how to make Swift repeat things — and just as importantly, how to control exactly when and how it repeats them. Here’s a quick recap:

    • For loops with ranges — use 1...10 (closed) or 1..<10 (half-open) to repeat code a set number of times; use stride to count by custom steps
    • For loops over arrays — loop through any collection with for item in array; use enumerated() when you need the index too
    • While loops — repeat code until a condition becomes false; use repeat-while when you always need at least one pass
    • Break and continuebreak exits the entire loop, continue skips to the next pass; labeled break lets you exit a specific outer loop
    • Nested loops — a loop inside a loop processes every combination; be mindful of how quickly the iteration count multiplies
    • Use var for any variable you change inside a loop, let for anything that stays fixed

    If any of these still feel shaky, go back and redo the challenge for that lesson before moving on. The challenges are the real learning — watching code run is one thing, writing it from scratch is another.

    In Stage 4 you’ll go deep on collections — arrays, dictionaries, and sets — and learn how to work with them using Swift’s powerful built-in tools like map, filter, and reduce. Those tools will replace many of the patterns you wrote as loops in this stage, and you’ll start to see why Swift developers prefer them.

    Learn Swift Stage 4: Organizing Code with Functions

    Functions are the moment Swift stops feeling like a list of instructions and starts feeling like a real programming language — and once they click, you won’t be able to imagine writing code without them.

    To follow along, you need to have finished Stages 1 through 3. That means you’re comfortable with variables, data types, if/else, switch, and loops. Each lesson takes about 30 minutes if you pause to type the examples yourself and attempt the challenge at the end. Do not skip the challenges — they are where the concepts actually land.

    By the end of Stage 4, you will know how to define and call a function, pass information into one, get information back out, set default values for parameters, and understand why a variable defined inside a function is invisible to the rest of your code. Functions are the biggest mental leap in this entire curriculum — but they are also the leap that makes everything else make sense. Take your time here.

    04
    Stage 4
    Organizing Code with Functions
    7 lessons · ~3.5 hrs
    4.1
    What is a Function?
    ⏱ 30 min Functions

    Imagine you work at a coffee shop. Every morning when a customer orders a latte, you steam the milk, pull the espresso shot, combine them, add the lid. You do the exact same six steps every single time. Now imagine if every barista had to re-invent the latte procedure from scratch for each order — writing out every step on a notepad before they started. That would be absurd. Instead, the shop trains everyone once and says: “when someone orders a latte, do this.” That training is the function. The order is calling it.

    In code, a function is a named block of instructions that you write once and run as many times as you need. Without functions, every time you wanted to do the same thing you would copy and paste the same lines over and over. That gets messy fast. Functions solve this with a principle programmers call DRY: Don’t Repeat Yourself.

    Here is the problem without functions:

    // Greeting three users — without a function
    var name1 = "Alice"
    print("Hello, \(name1)! Welcome to the app.")
    print("We're glad you're here, \(name1).")
    
    var name2 = "Bob"
    print("Hello, \(name2)! Welcome to the app.")
    print("We're glad you're here, \(name2).")
    
    var name3 = "Carol"
    print("Hello, \(name3)! Welcome to the app.")
    print("We're glad you're here, \(name3).")
    Output
    Hello, Alice! Welcome to the app.
    We’re glad you’re here, Alice.
    Hello, Bob! Welcome to the app.
    We’re glad you’re here, Bob.
    Hello, Carol! Welcome to the app.
    We’re glad you’re here, Carol.

    It works — but notice how you repeated the same two print lines three times. Now imagine changing the greeting wording. You would have to edit it in three places and hope you don’t miss one. This is exactly the kind of problem that leads to bugs. Functions fix it by letting you define the instructions once, name them, and just call that name.

    You have already been calling functions without realising it. print() is a function. Apple wrote it once, named it print, and every time you type print("something") you are calling their function. Starting in Lesson 4.2, you will write your own.

    ConceptPlain English meaning
    DRY“Don’t Repeat Yourself” — the principle that says the same logic should live in exactly one place in your code.
    FunctionA named, reusable block of code. Define it once, call it as many times as you need.
    DefineWriting the function — giving it a name and writing the instructions inside it.
    CallRunning the function — telling Swift to execute those instructions right now.
    Why this matters for iOS: Every iOS app is built from hundreds of functions working together. A button tap calls a function. Loading a screen calls a function. Saving data calls a function. Learning to think in functions is literally learning to think like an iOS developer.
    When to reach for a function Three clear signals that a function would help
    • You’re copying and pasting code. If you find yourself writing the same block twice, that block wants to be a function.
    • A block of code has one clear job. If you can describe what it does in five words or fewer, it’s a good function candidate.
    • You want to give a block of code a readable name. Functions make code read like plain English: greetUser() is much clearer than ten lines of print statements.
    Challenge 4.1 — Spot the Repetition

    Look at the six-line greeting code above. Without running it, rewrite it mentally as if you had a function called greetUser that handled those two print lines. How many times would you call it? What would you pass in? You don’t know the syntax yet — just think through the logic in plain English or pseudocode. Write your answer as a comment in a new Playground file.

    Hint: Focus on what changes each time (the name) vs what stays the same (the two print lines). The thing that changes is what you’ll eventually pass in as a parameter.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    Explain the DRY principle to me like I’m a beginner who has never written a function before. Use a real-world analogy that isn’t about cooking or recipes — try something from daily life. Then explain why violating DRY leads to bugs.
    I understand that print() is a function I’ve already been calling. Can you give me three more examples of things in everyday software (not Swift specifically) that are probably implemented as functions behind the scenes? Explain each one in plain English — what the function probably does, what it takes in, and what it gives back.
    Build a Practice Example Study AI-generated code with heavy comments
    Show me a simple Swift Playground with code that repeats itself badly — at least three blocks of nearly identical code doing the same thing. Add comments explaining why this code is fragile and hard to maintain. Don’t refactor it yet — I want to feel the pain first.
    Now show me the same logic rewritten using a function. Add a comment on every single line explaining what it does. Make the comments beginner-friendly — assume I have never seen a Swift function before.
    4.2
    Defining and Calling Functions
    ⏱ 30 min Functions

    Think of a function like a light switch. Someone wired up all the circuitry behind the wall — that wiring is the function definition. Once it’s done, anyone can flip the switch to turn the light on — that’s calling the function. The wiring only has to happen once. The flipping can happen a thousand times.

    In Swift, you define a function using the func keyword, give it a name, add parentheses, and put the code you want to run inside curly braces. Here is the simplest possible function:

    func sayHello() {                          // Define the function — wire up the switch
        print("Hello! Welcome to the app.")  // The code that runs when the function is called
    }                                          // End of the function body
    
    sayHello()  // Call the function — flip the switch (first time)
    sayHello()  // Call it again (second time) — same instructions, zero extra code
    sayHello()  // And again (third time)
    Output
    Hello! Welcome to the app.
    Hello! Welcome to the app.
    Hello! Welcome to the app.

    Three calls, three outputs, and the actual instructions only lived in one place. If you want to change the greeting wording, you change it once inside the function and all three calls update automatically.

    PartWhat it does
    funcThe Swift keyword that tells the compiler you are defining a function. It must come first.
    sayHelloThe name you give the function. Use camelCase. Pick a name that describes what the function does — ideally a verb or verb phrase.
    ()Parentheses after the name. Required even when the function takes no input. This is where parameters live — more on those in Lesson 4.3.
    { }Curly braces wrap the function body — the block of code that runs whenever the function is called.
    sayHello()Calling the function. Write the name followed by parentheses. Swift executes the function body right now at this line.
    Common mistake: Defining a function and forgetting to call it. If you write func sayHello() { ... } but never write sayHello(), nothing happens. The definition only sets up the instructions — calling it is what actually runs them.
    func name() Basic function — no input, no output
    func showWelcomeMessage() {
        print("Welcome!")
        print("Tap anywhere to get started.")
    }
    
    showWelcomeMessage()
    The most basic form. The function body can contain as many lines as you need — not just one. Group all the related steps together.
    Calling multiple times One definition, many calls
    func drawSeparator() {
        print("──────────────")
    }
    
    print("Section One")
    drawSeparator()
    print("Section Two")
    drawSeparator()
    print("Section Three")
    drawSeparator()
    Call the same function as many times as you need. Each call runs the body independently. Change the separator string in one place and every separator updates.
    Function calling a function Functions can call other functions inside their body
    func printTitle() {
        print("My App")
    }
    
    func launchScreen() {
        printTitle()               // Calls another function from inside this one
        print("Loading...")
    }
    
    launchScreen()
    Functions can call other functions. This is how real apps are structured — layers of functions, each doing one clear job, with higher-level functions coordinating the lower-level ones.
    Challenge 4.2 — Your First Function

    Write a function called showAppInfo that prints three lines: your app’s name, a one-sentence description, and the current version number. Then call it twice in a row. Confirm that both calls produce the same three lines of output.

    Hint: Start with func showAppInfo() { and put three print() calls inside. Don’t forget the closing curly brace. Then call it after the definition.
    Deepen Your Understanding Solidify the definition vs call distinction
    What is the difference between defining a function and calling a function in Swift? Explain it using a non-code analogy, then show me a two-line Swift example that illustrates both. Walk through each line.
    Why do Swift function names use camelCase? What are the naming conventions for Swift functions, and can you show me three examples of good function names vs three examples of poor ones — with an explanation of what makes each good or bad?
    Build a Practice Example Generate beginner-commented code to study
    Write a short Swift Playground with three simple functions — each one with a clear single job. Call each function at least twice. Add a comment on every line explaining what it does. Make this for someone who has never seen a function before today.
    Show me a Swift example where one function calls two other functions inside its body. Use a theme from a simple app (like a quiz app or a to-do list). Add inline comments explaining the order of execution — which line runs first, second, third, and so on.
    4.3
    Parameters
    ⏱ 30 min Functions

    Go back to the vending machine. A basic function is like a machine with one button that always gives you the same thing — a can of cola every time, no choices. That is useful, but limited. A real vending machine lets you pick what you want by pressing a button for a specific item. The item number you press is the parameter — the information you send into the machine to customise what comes out.

    Parameters let a function do slightly different work each time it is called, based on the information you pass in. That is what transforms a simple function into something genuinely powerful.

    func greetUser(name: String) {        // 'name' is the parameter — it has a label and a type
        print("Hello, \(name)!")           // Use the parameter like a regular variable inside
        print("We're glad you're here.")    // This line is the same every call
    }
    
    greetUser(name: "Alice")  // Call 1 — pass "Alice" as the name argument
    greetUser(name: "Bob")    // Call 2 — same function, different input
    greetUser(name: "Carol")  // Call 3 — same function, different input again
    Output
    Hello, Alice!
    We’re glad you’re here.
    Hello, Bob!
    We’re glad you’re here.
    Hello, Carol!
    We’re glad you’re here.

    This is the greeting problem from Lesson 4.1 — solved. One function definition, three calls, zero repeated code, and changing the greeting wording now requires editing exactly one place.

    PartWhat it does
    name:The parameter label. This is the word you use at the call site to make the code read naturally: greetUser(name: "Alice") reads almost like plain English.
    StringThe parameter type. Swift needs to know what kind of value this parameter holds. Here it’s a String, but it could be Int, Double, Bool, or any other type.
    name inside the bodyInside the function body, name works exactly like a constant holding whatever value was passed in at the call site.
    name: "Alice" at call siteThe argument — the actual value you pass in when calling the function. The label name: must match what the function definition declared.
    Parameter vs argument: These words are often used interchangeably by beginners, but they mean different things. A parameter is the placeholder in the function definition (name: String). An argument is the actual value you pass when calling it ("Alice"). Think: parameter is the slot, argument is what you put in the slot.
    Int parameter Parameters can be any type — not just String
    func printScore(score: Int) {
        print("Your score is \(score) points.")
    }
    
    printScore(score: 42)
    printScore(score: 100)
    Swap String for Int, Double, Bool, or any other type. The function body treats the parameter like a constant of that type.
    Bool parameter Use a parameter to control conditional logic inside the function
    func showStatus(isLoggedIn: Bool) {
        if isLoggedIn {
            print("Welcome back!")
        } else {
            print("Please sign in.")
        }
    }
    
    showStatus(isLoggedIn: true)
    showStatus(isLoggedIn: false)
    Parameters can drive if/else logic inside the function. The same function produces different output based on what you pass in — this is where functions start feeling genuinely powerful.
    _ underscore label Omit the label at the call site for cleaner syntax
    func printLine(_ text: String) {    // The _ means "no label required when calling"
        print(text)
    }
    
    printLine("Hello!")    // No label needed — reads cleanly
    printLine("Goodbye!")
    The underscore _ as the external label tells Swift not to require a label at the call site. Apple uses this pattern a lot — print("hello") works this way. Use it when the label would feel redundant.
    Challenge 4.3 — Personalised Messages

    Write a function called celebrateBirthday that takes a name parameter of type String and an age parameter of type Int. Inside, print a personalised birthday message using both values. Call it three times with different names and ages.

    Hint: You learned multiple parameters are coming in Lesson 4.5 — but try to figure it out now. Separate parameters with a comma inside the parentheses: func celebrateBirthday(name: String, age: Int). String interpolation works the same way as always inside the function body.
    Deepen Your Understanding Clarify parameters, arguments, and labels
    What is the difference between a parameter and an argument in Swift? I keep seeing these terms used interchangeably. Give me a clear explanation and a short code example that labels which part is the parameter and which part is the argument.
    Why does Swift use parameter labels at the call site — like greetUser(name: “Alice”) instead of just greetUser(“Alice”)? What problem does this solve? Show me an example where the label makes the code clearer and one where using _ to drop the label makes more sense.
    Build a Practice Example Study parameterised functions with full comments
    Write three Swift functions, each with one parameter of a different type (String, Int, Bool). For each function, show two different calls with different argument values. Add a comment on every line explaining what’s happening, written for a beginner.
    Write a Swift function that uses an if/else statement inside its body, with a Bool parameter controlling which branch runs. Show four calls with alternating true and false arguments. Add comments throughout explaining the flow of execution.
    4.4
    Return Values – Getting Information Back Out
    ⏱ 30 min Functions

    So far, functions have been a one-way street. You send information in (via parameters) and the function does something with it — but it doesn’t hand anything back to you. Return values change that. A function with a return value works like a calculator: you give it numbers, it does the maths, and it hands back the result. You can then use that result anywhere in your code.

    The arrow -> after the parentheses declares what type of value the function will return. The return keyword sends that value back to whoever called the function.

    func addNumbers(a: Int, b: Int) -> Int {   // -> Int declares the return type
        let result = a + b                          // Do the work
        return result                               // Send the result back to the caller
    }
    
    let total = addNumbers(a: 10, b: 25)   // The returned value is stored in 'total'
    print("Total: \(total)")                 // Use it like any other variable
    
    let anotherTotal = addNumbers(a: 3, b: 7)
    print("Another total: \(anotherTotal)")
    Output
    Total: 35
    Another total: 10
    PartWhat it does
    -> IntThe return type declaration. The arrow followed by a type tells Swift what kind of value this function will hand back. Every path through the function must return this type.
    return resultSends the value of result back to wherever the function was called. After return, the function stops immediately — nothing else in the body runs.
    let total = addNumbers(...)The call site captures the returned value into a constant. The function call on the right side evaluates to the return value, just like any expression.
    Return exits immediately: Once Swift hits a return statement, the function stops. Any code written after return inside the same block will never run. This is sometimes used on purpose — for example, returning early if a condition isn’t met.
    -> String Return a String instead of computing it at the call site
    func buildGreeting(name: String) -> String {
        let message = "Hello, \(name)! Welcome back."
        return message
    }
    
    let greeting = buildGreeting(name: "Alice")
    print(greeting)
    Return a String when the function’s job is to build or format text. The caller gets a ready-to-use string and doesn’t need to know how it was built.
    -> Bool Return true or false — great for validation logic
    func isAdult(age: Int) -> Bool {
        return age >= 18           // Returns true if age is 18 or over, false otherwise
    }
    
    if isAdult(age: 21) {
        print("Access granted.")
    } else {
        print("Access denied.")
    }
    Returning Bool is perfect for validation checks. You can use the function call directly inside an if statement — no intermediate variable needed.
    Implicit return Single-expression functions can omit the return keyword
    func square(_ number: Int) -> Int {
        number * number   // No 'return' keyword — Swift infers it for single expressions
    }
    
    print(square(5))   // Prints 25
    When a function body is a single expression, you can leave out return. Swift automatically returns the value of that expression. This is called implicit return and you’ll see it often in SwiftUI code.
    Challenge 4.4 — Temperature Converter

    Write a function called celsiusToFahrenheit that takes a Double parameter called celsius and returns a Double. The formula is: fahrenheit = (celsius * 9/5) + 32. Call it with at least three values (try 0, 100, and 37) and print each result with a descriptive label.

    Hint: The return type goes after the closing parenthesis: func celsiusToFahrenheit(celsius: Double) -> Double. Make sure to use 9.0/5.0 or Double(9)/Double(5) to avoid integer division giving you a wrong answer.
    Deepen Your Understanding Understand return values and what they enable
    What does the return keyword actually do in Swift? Explain what happens step by step when a function with a return value is called — from the moment the call is made to the moment the returned value is available at the call site.
    What is the difference between a function that prints a value and a function that returns a value? They both produce output — but why does it matter which one you use? Explain with a simple example where printing would be the wrong choice and returning is the right one.
    Build a Practice Example Study return values with heavy inline comments
    Write three Swift functions — one returning a String, one returning an Int, one returning a Bool. For each function, show how the returned value is captured in a constant and then used. Add a comment on every single line, written for a beginner who has never seen a return value before.
    Write a Swift example where a function’s return value is used directly inside an if statement without being stored in a variable first — like if isAdult(age: 20) { }. Show two different functions used this way. Explain in comments why this works and when you’d prefer this style over storing the result first.
    4.5
    Multiple Parameters
    ⏱ 30 min Functions

    A coffee order has multiple details: size, type, milk preference, number of shots. The barista needs all of them to make the right drink. A function can receive multiple pieces of information in the same way — you simply list multiple parameters inside the parentheses, separated by commas. Each parameter has its own label and type.

    func describeProduct(name: String, price: Double, inStock: Bool) -> String {
        let availability = inStock ? "In stock" : "Out of stock"   // Ternary from Stage 2
        return "\(name) — $\(price) — \(availability)"
    }
    
    let desc1 = describeProduct(name: "Swift Book", price: 29.99, inStock: true)
    let desc2 = describeProduct(name: "iOS Course", price: 49.00, inStock: false)
    print(desc1)
    print(desc2)
    Output
    Swift Book — $29.99 — In stock
    iOS Course — $49.0 — Out of stock
    PartWhat it does
    name: String, price: Double, inStock: BoolThree parameters, each with a label and type, separated by commas. Swift needs all three when the function is called.
    Order at the call siteArguments must be passed in the same order as they are declared in the function signature. You can’t swap them around.
    Each parameter is independentInside the function body, each parameter works like its own constant with its own name and type. They don’t interfere with each other.
    Order matters: Swift matches arguments to parameters by position. describeProduct(name: "X", price: 9.99, inStock: true) is valid. Writing the arguments in a different order, like price before name, is a compile error. The labels help you remember the order — and help the compiler catch mistakes.
    External + internal labels Give a parameter different names at the call site vs inside the body
    func greet(user name: String) {  // 'user' is the external label, 'name' is used inside
        print("Hello, \(name)!")        // Inside the body, use 'name'
    }
    
    greet(user: "Alice")              // At the call site, use 'user'
    Swift lets you have two names for the same parameter: an external label used at the call site (for readability), and an internal name used inside the function body. This is how you write functions that read like English: greet(user: "Alice").
    Mixed types Parameters can be any combination of types
    func calculateTip(billAmount: Double, tipPercent: Double) -> Double {
        return billAmount * (tipPercent / 100)
    }
    
    let tip = calculateTip(billAmount: 45.50, tipPercent: 18)
    print("Tip: $\(tip)")
    Mix and match parameter types freely. The function signature makes it clear what each one is, and the parameter labels at the call site document the intent.
    Challenge 4.5 — Rectangle Calculator

    Write two functions: rectangleArea(width: Double, height: Double) -> Double and rectanglePerimeter(width: Double, height: Double) -> Double. Call each with at least two different sets of dimensions and print both results for each. Bonus: write a third function called describeRectangle that calls both of the above and prints a formatted summary of the area and perimeter.

    Hint: Area = width * height. Perimeter = 2 * (width + height). The bonus function is a great example of a function calling other functions.
    Deepen Your Understanding Understand external and internal parameter labels
    Explain the difference between an external parameter label and an internal parameter name in Swift. Why does Swift have both? Show me a function definition with both, and then show the call site so I can see exactly which label appears where.
    I know that I can use _ to drop the external label on a parameter. When should I do this and when should I keep the label? Show me examples where each choice makes the code more readable.
    Build a Practice Example Study multi-parameter functions with full comments
    Write a Swift function with three parameters of different types (at least one String, one numeric, one Bool). Show three different calls with different argument values. Add a comment on every line explaining what happens, written for someone learning functions for the first time today.
    Write a realistic iOS-themed example — like a function for formatting a user profile summary, or generating an order confirmation message — that takes four or five parameters. Use external labels that make the call site read naturally. Add inline comments throughout.
    4.6
    Default Parameter Values
    ⏱ 30 min Functions

    Think about ordering a coffee. If you just say “a latte, please” without specifying size, the barista gives you a medium. Medium is the default. If you want something different, you say so — but you don’t have to. Default parameter values work exactly the same way. You assign a value to a parameter in the function definition, and then the caller can either pass their own value or leave it out entirely to use the default.

    func sendWelcomeEmail(to name: String, subject: String = "Welcome!") {
        // 'subject' has a default value — the caller doesn't have to provide it
        print("To: \(name)")
        print("Subject: \(subject)")
        print("---")
    }
    
    sendWelcomeEmail(to: "Alice")                              // Uses default subject
    sendWelcomeEmail(to: "Bob", subject: "You're In!")        // Overrides the default
    sendWelcomeEmail(to: "Carol")                            // Uses default subject again
    Output
    To: Alice
    Subject: Welcome!
    To: Bob
    Subject: You’re In!
    To: Carol
    Subject: Welcome!
    PartWhat it does
    subject: String = "Welcome!"Declares a parameter with a default value. The = "Welcome!" part sets what Swift uses if the caller doesn’t provide that argument.
    Call without the argumentWhen you omit a parameter that has a default, Swift substitutes the default value automatically. The function behaves as if you passed it in.
    Call with the argumentWhen you do provide the argument, your value replaces the default for that specific call only.
    Put defaulted parameters last: As a convention, parameters with default values should go at the end of the parameter list. This means callers who want the simple version can just skip the end, while those who need to customise can add the extra arguments. Swift does not enforce this as a rule, but it is the accepted pattern.
    Multiple defaults More than one parameter can have a default value
    func formatPrice(amount: Double, currency: String = "USD", showDecimals: Bool = true) {
        if showDecimals {
            print("\(currency) \(amount)")
        } else {
            print("\(currency) \(Int(amount))")
        }
    }
    
    formatPrice(amount: 9.99)                                     // USD 9.99
    formatPrice(amount: 9.99, currency: "EUR")                   // EUR 9.99
    formatPrice(amount: 9.99, currency: "GBP", showDecimals: false) // GBP 9
    Multiple defaults give callers flexibility. The simplest call only provides what’s required; more specific calls add the optional parameters they need. This pattern is extremely common in real iOS APIs.
    Default Bool Defaults are great for toggles and optional behaviours
    func logMessage(_ message: String, verbose: Bool = false) {
        print("[LOG] \(message)")
        if verbose {
            print("  → Details: message length = \(message.count)")
        }
    }
    
    logMessage("App launched")              // Simple — verbose defaults to false
    logMessage("User tapped button", verbose: true)  // Enables extra output
    A Bool with a default of false is a classic way to add optional behaviour. Most callers get the simple path; those who need more detail opt in explicitly.
    Challenge 4.6 — Notification Builder

    Write a function called buildNotification that takes three parameters: title: String, body: String, and badge: Int with a default value of 1. It should return a formatted String like “TITLE — body (badge: 1)”. Call it four times: once with all three arguments, once omitting the badge, and twice more of your choosing. Print each result.

    Hint: The default goes right in the parameter declaration: badge: Int = 1. When you want to test the default, just don’t include badge: in that call.
    Deepen Your Understanding Understand when and why to use defaults
    When should I give a parameter a default value and when should I require the caller to always provide it? Give me a real-world iOS app scenario where a default value is the right design choice, and one where making the parameter required would be better — with a clear explanation of why.
    Swift’s own APIs use default parameter values all over the place. Can you show me two examples from the Swift standard library or SwiftUI where a function or initialiser uses default parameter values, and explain what the default does in each case?
    Build a Practice Example Study default parameters with graduated examples
    Write a Swift function with two required parameters and two optional (defaulted) parameters. Show four different calls: one using all four, one using only the required ones, and two that mix and match. Add a comment on every line explaining what’s being passed and which value is used.
    Write a realistic iOS function — something a real app might use, like formatting a date, building a URL, or creating a settings entry — that uses default parameters intelligently. Show multiple call-site variations and explain in comments why the defaults were chosen.
    4.7
    Understanding Scope — What Variables a Function Can and Can’t See
    ⏱ 30 min Functions

    Imagine two different conference rooms in an office building. A conversation happening in Room A is completely private — nobody in Room B can hear it, and nothing said in Room A automatically affects what’s happening in Room B. Both rooms exist in the same building, but they are separate spaces. This is scope: the idea that variables only exist and are only visible inside the space where they were created.

    In Swift, every set of curly braces creates its own scope. A variable declared inside a function exists only for the life of that function call. The moment the function finishes, those variables disappear. Code outside the function cannot see them, and code inside the function cannot see variables from other functions.

    var appName = "MyApp"              // Declared at the top level (global scope)
    
    func showAppName() {
        print(appName)                  // Can access appName — it's in a wider scope
        var localMessage = "Hello!"    // Only exists inside this function
        print(localMessage)
    }
    
    showAppName()
    
    // print(localMessage)  ← This would be a compile error!
    // localMessage doesn't exist outside the function
    Output
    MyApp
    Hello!
    func functionOne() {
        var secret = "I'm in function one"
        print(secret)
    }
    
    func functionTwo() {
        // print(secret)  ← Compile error — 'secret' doesn't exist here
        var secret = "I'm in function two"  // Totally separate variable, same name is fine
        print(secret)
    }
    
    functionOne()
    functionTwo()
    Output
    I’m in function one
    I’m in function two

    Notice that both functions have a variable called secret. They are completely separate variables that happen to share a name. Changing one has zero effect on the other. This is one of the most important and useful features of scope — it means you don’t have to worry about naming conflicts between functions.

    ConceptPlain English meaning
    ScopeThe region of code where a variable can be seen and used. Defined by the curly braces that contain it.
    Local variableA variable declared inside a function. Only visible inside that function. Disappears when the function returns.
    Global variableA variable declared outside all functions, at the top level. Visible everywhere in the file. Use sparingly — it creates dependencies between parts of your code.
    ShadowingDeclaring a local variable with the same name as an outer-scope variable. The local one takes priority inside that scope. Swift allows this but it can be confusing — avoid it when possible.
    Why scope is a feature, not a bug: Scope might feel like a restriction at first. Why can’t every function just see every variable? Because if they could, changing a variable in one place could silently break code everywhere else. Scope keeps functions self-contained and predictable — one of the most important qualities in any codebase.
    Nested scope if and for blocks create their own scope too
    func checkScore(score: Int) {
        if score >= 80 {
            let grade = "Pass"        // 'grade' only exists inside this if block
            print("Grade: \(grade)")
        }
        // print(grade)  ← Compile error — 'grade' is out of scope here
        print("Score checked.")
    }
    
    checkScore(score: 90)
    It’s not just functions — if, for, while, and switch blocks all create their own scope. A variable declared inside one of those blocks cannot be accessed outside it.
    Passing is not sharing Passing a variable to a function gives it a copy, not access to the original
    func doubleIt(_ number: Int) -> Int {
        let result = number * 2   // 'number' here is a local copy of whatever was passed in
        return result
    }
    
    var myNumber = 10
    let doubled = doubleIt(myNumber)   // Passes the VALUE of myNumber — not the variable itself
    print(myNumber)   // Still 10 — the function didn't touch the original
    print(doubled)    // 20
    When you pass a variable to a function, Swift passes a copy of the value (for basic types like Int, String, Double, Bool). The function works with that copy. The original variable is unchanged. This is called value semantics — and it is one of the reasons Swift code is predictable.
    Challenge 4.7 — Scope Detective

    In a new Playground, write three functions: functionA, functionB, and functionC. In each one, declare a local variable called count with a different value (1, 2, and 3). Print count from inside each function. Call all three. Then try adding a line after the functions that prints count — observe the compile error, then comment it out. Write a comment explaining in your own words why the error occurred.

    Hint: The goal isn’t to fix the error — it’s to trigger it intentionally and observe it. Reading and understanding compiler errors is one of the most important skills in Swift development.
    Deepen Your Understanding Understand scope through explanation and analogies
    Explain variable scope in Swift to me using a real-world analogy. Then show me a simple code example with a global variable, a local variable inside a function, and a local variable inside an if block. Explain which parts of the code can and can’t see each variable.
    What happens when I declare two variables with the same name in different functions in Swift? Walk me through exactly what the compiler does and explain why this is allowed. Is there any situation where this could cause confusion or a bug?
    Build a Practice Example Visualise scope through annotated code
    Write a Swift example that demonstrates at least three different levels of scope — global, function-level, and block-level (inside an if or for). Use comments to visually mark where each variable’s scope begins and ends. Make it clear which lines can see which variables.
    Show me a Swift example where a developer accidentally tries to use a variable outside its scope. Include the compiler error message that Swift would show. Then show the correct way to fix the code — either by moving the declaration or by using a return value — and explain why each fix works.
    Audit Your Own Code Get feedback on scope decisions in code you’ve written
    Here’s some Swift code I wrote: [paste your code]. Can you identify any variables that have a wider scope than they need to? For each one, explain where the variable is currently declared, where it actually needs to be visible, and whether moving it to a tighter scope would make the code safer or cleaner.
    Review this code for how I’m using global variables: [paste your code]. Am I overusing them? Are there places where I should be passing values as function parameters or return values instead of relying on variables that live outside the function? Explain your reasoning before suggesting any changes.

    Stage 4 Recap: Organizing Code with Functions

    Functions are the most important concept you have learned so far — and the one that will change how you see code forever. Here’s what you now understand:

    • A function is a named, reusable block of code. You define it once and call it as many times as you need — this is the DRY principle in action.
    • You define a function with the func keyword, a name, parentheses, and a body wrapped in curly braces. You call it by writing its name followed by ().
    • Parameters let you pass information into a function. Each parameter has a label and a type. At the call site, you provide the matching argument.
    • Return values let a function send a result back out. Use -> Type after the parentheses and the return keyword inside the body.
    • Functions can have multiple parameters, each with their own label and type, separated by commas.
    • Default parameter values let callers skip optional arguments — use = value in the parameter declaration. Put defaulted parameters last.
    • Scope means a variable only exists where it was declared. Local variables inside a function are invisible to the outside world — and that’s a feature, not a limitation.
    • When you pass a basic value (Int, String, Double, Bool) to a function, Swift passes a copy. The original is untouched.

    Functions are the building blocks that everything else in Swift rests on. Every concept coming in Stages 5 and beyond — closures, structs, classes, SwiftUI views — builds directly on what you just learned. If anything in this stage felt unclear, revisit that lesson before moving on. The time you spend here pays off ten times over later.

    Stage 5 covers closures — which you can think of as functions without names that can be passed around like values. They are everywhere in Swift and in SwiftUI, and now that you understand functions, closures will make much more sense.

    Learn Swift Stage 5: Collections

    Most real apps aren’t about one piece of data — they’re about lists of things, and this stage is where you learn to work with them.

    To follow along, open Xcode and create a new Swift Playground. Each lesson takes around 30 minutes and ends with a challenge you’ll solve on your own. That challenge is where the actual learning happens, so don’t skip it.

    By the end of Stage 5 you’ll understand how to create and use arrays, dictionaries, and sets — the three core collection types in Swift. These are the building blocks every real app relies on. When your app loads a list of products, stores user settings, or tracks unique tags, it’s using collections.

    05
    Stage 5
    Collections
    5 lessons · ~2.5 hrs
    5.1
    Arrays – What They Are and How to Create Them
    ⏱ 30 min Swift Collections

    Imagine you’re writing a shopping list. You could write each item on a separate sticky note, but that gets messy fast. Instead you write them all on one list, in order. That’s an array.

    An array is an ordered collection of values of the same type. Instead of creating five separate variables to hold five names, you put all five names into one array. Swift keeps them in the order you added them, and you can access any item by its position in the list.

    That position is called an index. Array indexes always start at 0, not 1. So the first item is at index 0, the second is at index 1, and so on. This trips up almost every beginner at least once, so keep it in mind.

    // Creating an array of strings
    var shoppingList: [String] = ["Apples", "Bread", "Milk"]
    
    // Swift can infer the type — this works too
    var scores = [98, 74, 85, 91]
    
    // Creating an empty array — you must declare the type
    var names: [String] = []
    
    // Accessing items by index (remember: starts at 0)
    print(shoppingList[0])  // "Apples"
    print(shoppingList[2])  // "Milk"
    
    // Arrays have a count property
    print(shoppingList.count)  // 3
    Output
    Apples
    Milk
    3
    LineWhat it does
    var shoppingList: [String] = [...]Creates an array called shoppingList that holds String values. The square brackets around the type tell Swift this is an array.
    var scores = [98, 74, 85, 91]Swift sees four integers and infers the type is [Int] automatically. You don’t always need to write the type yourself.
    var names: [String] = []Creates an empty array. When there are no values to infer the type from, you must write it explicitly.
    shoppingList[0]Reads the item at index 0. Square brackets with a number let you access any position in the array.
    .countA property built into every array that tells you how many items it contains.
    Watch out: If you try to access an index that doesn’t exist — for example shoppingList[10] on a 3-item array — your app will crash at runtime. Swift won’t catch this as a compile error. Always make sure the index you’re using is within range.
    [Type]() Another way to create an empty array
    var tags = [String]()
    This is equivalent to var tags: [String] = []. Both create an empty String array. You’ll see both styles in real Swift code, so it’s worth recognising each form.
    Array(repeating:count:) Create an array pre-filled with a repeated value
    var zeros = Array(repeating: 0, count: 5)
    // Result: [0, 0, 0, 0, 0]
    Useful when you need a fixed-size array with a default value to start from. Common in algorithms and games where you need a board or grid of initial values.
    let vs var Constant vs mutable arrays
    let fixedList = ["A", "B", "C"]  // cannot be changed
    var growingList = ["A", "B"]     // can add or remove items
    Use let when the array’s contents won’t change after creation. Use var when you need to add, remove, or update items. This is the same rule as regular variables.
    Challenge 5.1 Your Favourite Films

    Create an array called favouriteFilms that holds the titles of three films you enjoy. Then print the first film, the last film, and the total count of films in the array.

    Hint: The last item in a 3-item array is at index 2. Remember that .count gives you the number of items, not the last valid index.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain Swift arrays to me like I’m a beginner. Use a real-world analogy, then show me the simplest possible example and walk through it line by line. Don’t write any code until you’ve explained the concept in plain English first.
    I understand that Swift array indexes start at 0, but I want to make sure I really get it. Can you quiz me? Ask me which index various items are at in a sample array, one question at a time, and tell me if I get something wrong.
    Build a Practice Example Study commented code to understand the why, not just the what
    Write a short Swift example that creates an array of strings, accesses a few items by index, and uses .count. Add a comment on every single line explaining what it does and why — write the comments for someone who has never used arrays before.
    Give me three different Swift arrays — one of strings, one of integers, and one created empty. For each one, show me how to access the first item and print the count. Add inline comments throughout explaining the pattern.
    5.2
    Arrays Operations
    ⏱ 30 min Swift Collections

    Now that you can create an array, you need to be able to do things with it. In a real app you’re constantly adding new items, removing old ones, checking whether something is in the list, and finding the first or last entry.

    Swift gives you a set of built-in methods and properties that handle all of this. They’re built right into every array, so you call them using dot syntax — the same way you call .count.

    var fruits = ["Apple", "Banana", "Cherry"]
    
    // append — adds one item to the end
    fruits.append("Mango")
    print(fruits)  // ["Apple", "Banana", "Cherry", "Mango"]
    
    // insert — adds an item at a specific index
    fruits.insert("Kiwi", at: 1)
    print(fruits)  // ["Apple", "Kiwi", "Banana", "Cherry", "Mango"]
    
    // remove — removes the item at a specific index
    fruits.remove(at: 0)
    print(fruits)  // ["Kiwi", "Banana", "Cherry", "Mango"]
    
    // contains — checks if an item exists, returns true or false
    print(fruits.contains("Banana"))  // true
    print(fruits.contains("Apple"))   // false
    
    // first and last — optional properties
    print(fruits.first!)  // "Kiwi"
    print(fruits.last!)   // "Mango"
    
    // count
    print(fruits.count)  // 4
    Output
    [“Apple”, “Banana”, “Cherry”, “Mango”]
    [“Apple”, “Kiwi”, “Banana”, “Cherry”, “Mango”]
    [“Kiwi”, “Banana”, “Cherry”, “Mango”]
    true
    false
    Kiwi
    Mango
    4
    OperationWhat it does
    .append("Mango")Adds “Mango” to the end of the array. This is the most common way to grow an array.
    .insert("Kiwi", at: 1)Adds “Kiwi” at index 1. Everything after it shifts right by one position.
    .remove(at: 0)Removes the item at index 0. Everything after it shifts left by one position. Also returns the removed item if you want to use it.
    .contains("Banana")Returns true if the value is in the array, false if not. Great for checking membership without looping manually.
    .first and .lastThese are Optionals — they return nil if the array is empty. The ! force-unwraps them, which is safe only if you know the array isn’t empty.
    Why are first and last Optionals? Because the array might be empty. If you call .first on an empty array, there’s no first item — so Swift returns nil instead of crashing. In real code, you’d use if let firstFruit = fruits.first { ... } rather than force-unwrapping with !.
    += operator Append multiple items at once
    var numbers = [1, 2, 3]
    numbers += [4, 5, 6]
    print(numbers)  // [1, 2, 3, 4, 5, 6]
    The += operator lets you join two arrays together. You can also use + to combine arrays without modifying either one.
    .removeAll() Clear every item from the array
    var cart = ["Shirt", "Shoes", "Hat"]
    cart.removeAll()
    print(cart.count)   // 0
    print(cart.isEmpty) // true
    Use .removeAll() to empty an array completely. The .isEmpty property is a cleaner way to check if an array has no items — it’s more readable than writing count == 0.
    subscript assignment Update an item at a specific index
    var colors = ["Red", "Green", "Blue"]
    colors[1] = "Yellow"
    print(colors)  // ["Red", "Yellow", "Blue"]
    You can update any existing item by assigning directly to its index. This replaces the value in place — the array length stays the same. You cannot use this to add a new item beyond the array’s current size.
    .sorted() / .reversed() Get a reordered version of the array
    let raw = [5, 2, 8, 1]
    let ascending  = raw.sorted()           // [1, 2, 5, 8]
    let descending = raw.sorted(by: >)     // [8, 5, 2, 1]
    let flipped    = raw.reversed()        // sequence [1, 8, 2, 5]
    .sorted() returns a new sorted array and leaves the original unchanged. .sort() (no parentheses with arguments) sorts the array in place. Use by: > for descending order.
    Challenge 5.2 Build a To-Do List

    Start with an empty array called toDoList. Add three tasks to it using .append(). Then remove the second task. Finally, check whether “Buy groceries” is in the list using .contains() and print the result.

    Hint: The second task is at index 1. After removal, the array will have two items. Make sure “Buy groceries” was one of the tasks you originally appended, or the contains check will always return false.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain the difference between .append(), .insert(at:), and subscript assignment on a Swift array. When would I use each one? Give me plain English explanations before any code examples.
    I’m confused about why .first and .last are Optionals in Swift. Can you explain what an Optional is in this context and why Swift made this design choice? Don’t show me any code yet — just explain the reasoning.
    Build a Practice Example Study commented code to understand the why, not just the what
    Write a Swift example that starts with an array of five city names, then demonstrates append, insert, remove, and contains — in that order. Add a comment on every line explaining what is happening and why, written for a beginner who is seeing these methods for the first time.
    Show me a Swift example of a simple shopping cart that uses an array. It should add items, remove one, check if something is in the cart, and print the count. Add inline comments throughout. Keep it realistic — something I’d see in an actual iOS app.
    5.3
    Iterating Over Arrays with For Loops
    ⏱ 30 min Swift Collections

    In Stage 3 you learned how to loop over a range of numbers. Now you can combine that skill with arrays — and this is where things start to feel like real programming.

    One of the most common things you do with an array is go through every item and do something with each one. Maybe you print them all, filter some out, or calculate a total. Swift gives you a clean syntax for this that you’ll use in nearly every app you write.

    let temperatures = [22.5, 18.0, 30.1, 25.4, 19.8]
    
    // Basic for-in loop — reads every item one at a time
    for temp in temperatures {
        print("Temperature: \(temp)°C")
    }
    
    // Loop with a condition inside
    print("Hot days:")
    for temp in temperatures {
        if temp > 25.0 {
            print("\(temp)°C is a hot day")
        }
    }
    
    // Calculating a total using a running variable
    var total: Double = 0.0
    for temp in temperatures {
        total += temp   // add each temperature to total
    }
    let average = total / Double(temperatures.count)
    print("Average: \(average)°C")
    Output
    Temperature: 22.5°C
    Temperature: 18.0°C
    Temperature: 30.1°C
    Temperature: 25.4°C
    Temperature: 19.8°C
    Hot days:
    30.1°C is a hot day
    25.4°C is a hot day
    Average: 23.16°C
    LineWhat it does
    for temp in temperaturesEach time through the loop, Swift takes the next item from the array and puts it in temp. You pick the name — it could be anything readable.
    if temp > 25.0A condition inside a loop. This runs for every item, but only executes the print when the condition is true.
    var total: Double = 0.0A running total declared before the loop. Each iteration adds to it. This is a very common pattern — sometimes called an accumulator.
    total += tempShorthand for total = total + temp. After the loop finishes, total holds the sum of every temperature.
    Double(temperatures.count)Converts the count (an Int) to a Double so the division works correctly. Dividing two integers in Swift would give you a whole number result, losing any decimal.
    Name your loop variable well: If you’re looping over temperatures, name the loop variable temperature or temp. If you’re looping over products, use product. Singular vs plural is the convention and makes your loops much easier to read.
    enumerated() Loop with both the index and the value
    let players = ["Alice", "Bob", "Charlie"]
    for (index, player) in players.enumerated() {
        print("Player \(index + 1): \(player)")
    }
    // Player 1: Alice
    // Player 2: Bob
    // Player 3: Charlie
    .enumerated() gives you both the position and the value in each iteration. Indexes start at 0, so index + 1 makes the output start at 1. This is handy whenever you need to display numbered lists.
    indices Loop using index numbers directly
    let items = ["A", "B", "C"]
    for i in items.indices {
        print("Index \(i): \(items[i])")
    }
    // Index 0: A
    // Index 1: B
    // Index 2: C
    .indices gives you a range of valid index numbers for the array. This is safer than writing 0..<items.count yourself because Swift ensures the range always matches the array’s actual size.
    forEach Alternative iteration using a closure
    let names = ["Zara", "Leo", "Mia"]
    names.forEach { name in
        print("Hello, \(name)!")
    }
    .forEach works similarly to a for-in loop but uses closure syntax. You’ll encounter this style when working with SwiftUI and functional code. It cannot use break or continue — for that you need a regular for-in loop.
    Challenge 5.3 Grade Report

    Create an array of five integer test scores. Write a loop that prints each score along with its position number (starting from 1, not 0). Then write a second loop that only prints scores that are 70 or above, labelling them as “Pass”.

    Hint: For the first part, .enumerated() will save you some work. For the second part, a plain for-in loop with an if condition is all you need.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain the difference between a for-in loop over an array, using .enumerated(), and using .indices. When would I choose each approach? Give me plain English explanations and examples without just listing syntax.
    I wrote a loop that calculates the sum of an array, but I’m not sure I understand the accumulator pattern. Can you explain why you need a variable declared outside the loop, and what happens step by step on each iteration? Walk me through it without writing new code.
    Build a Practice Example Study commented code to understand the why, not just the what
    Write a Swift example that loops over an array of product names and prices and prints a formatted receipt. Add a comment on every line explaining what each part does. Make it feel realistic — something close to what you’d see in an iOS shopping app.
    Give me three different examples of looping over a Swift array — one using for-in, one using .enumerated(), and one using .forEach. For each, use a different real-world scenario and add inline comments explaining the choice of loop style.
    5.4
    Dictionaries – Key/Value Pairs
    ⏱ 30 min Swift Collections

    Think about a physical contact book. You don’t look someone up by their position in the book — you look them up by their name. “Alice” maps to her phone number. “Bob” maps to his. The name is the lookup key and the phone number is the value it points to.

    That’s exactly what a dictionary is in Swift. Instead of accessing data by a numbered index like an array, you access it by a meaningful key. Each key must be unique and maps to exactly one value.

    Dictionaries are unordered — Swift does not guarantee the items will come back in any particular sequence. If order matters, use an array. If lookup by a meaningful label matters, use a dictionary.

    // Creating a dictionary [KeyType: ValueType]
    var contactBook: [String: String] = [
        "Alice": "555-1234",
        "Bob":   "555-5678",
        "Carol": "555-9012"
    ]
    
    // Reading a value — returns an Optional
    if let phone = contactBook["Alice"] {
        print("Alice's number: \(phone)")
    }
    
    // Adding a new key/value pair
    contactBook["Dan"] = "555-3456"
    
    // Updating an existing value
    contactBook["Bob"] = "555-0000"
    
    // Removing a key/value pair
    contactBook["Carol"] = nil
    
    // Looping over a dictionary
    for (name, number) in contactBook {
        print("\(name): \(number)")
    }
    
    print("Total contacts: \(contactBook.count)")
    Output
    Alice’s number: 555-1234
    Alice: 555-1234
    Dan: 555-3456
    Bob: 555-0000
    Total contacts: 3
    LineWhat it does
    [String: String]The type annotation for a dictionary. The first type is the key type, the second is the value type. Keys are always the type before the colon.
    contactBook["Alice"]Looks up the value for the key “Alice”. This returns an Optional because the key might not exist. Always unwrap safely with if let.
    contactBook["Dan"] = "555-3456"If the key “Dan” doesn’t exist, this adds it. If it already exists, this updates its value. Adding and updating use identical syntax.
    contactBook["Carol"] = nilSetting a key to nil removes it from the dictionary entirely. After this line, “Carol” no longer exists as a key.
    for (name, number) in contactBookIterates over every key/value pair. Swift gives you both the key and the value as a tuple. The order of pairs is not guaranteed.
    Lookups always return an Optional: When you read from a dictionary, Swift can’t guarantee the key exists — so it returns an Optional. You should always use if let or a default value (contactBook["Alice"] ?? "Unknown") rather than force-unwrapping with !.
    default value Provide a fallback when the key might not exist
    let score = contactBook["Unknown", default: "No number"]
    print(score)  // "No number"
    When you provide a default: value, the lookup returns that instead of nil if the key is missing. The result is a regular String, not an Optional — so no unwrapping needed. Useful when missing keys are expected and you have a sensible fallback.
    keys and values Access all keys or all values separately
    let allKeys   = Array(contactBook.keys)
    let allValues = Array(contactBook.values)
    print(allKeys)    // e.g. ["Alice", "Bob", "Dan"] (order not guaranteed)
    print(allValues)  // e.g. ["555-1234", "555-0000", "555-3456"]
    .keys and .values give you collections of every key and every value. Wrapping them in Array() converts them into a regular array you can index or sort.
    mixed value types Store different value types using Any
    var userProfile: [String: Any] = [
        "name": "Alice",
        "age":  28,
        "isPremium": true
    ]
    Using Any as the value type lets you mix strings, integers, booleans, and more in one dictionary. You’ll see this when working with JSON data from an API. The downside is you lose type safety — use structs instead when you control the data model.
    Challenge 5.4 Country Capitals

    Create a dictionary called capitals that maps three country names to their capital cities. Then add a fourth country, update one of the existing capitals to a different city, and safely look up a capital using if let. Print a message like “The capital of France is Paris.”

    Hint: Remember that reading from a dictionary returns an Optional. Use if let capital = capitals["France"] and build your print statement inside the if block using the unwrapped value.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain the difference between arrays and dictionaries in Swift using a real-world analogy for each. When would I choose one over the other? Don’t write any code until you’ve explained the concept in plain English.
    Why does reading a value from a Swift dictionary return an Optional? Explain the reasoning behind this design decision. I want to understand why it’s Optional — not just that it is.
    Build a Practice Example Study commented code to understand the why, not just the what
    Write a Swift example of a dictionary that represents a simple product catalogue — product names as keys, prices as values. Show adding, updating, removing, and safely reading an item. Add a comment on every line explaining what is happening and why.
    Give me a Swift example that loops over a dictionary and prints each key/value pair in a readable format. Then show me how to get a sorted list of the keys. Add inline comments throughout explaining each step.
    5.5
    Arrays vs Dictionaries vs Sets – When to Use Which
    ⏱ 30 min Swift Collections

    You now know about arrays and dictionaries. There’s one more collection type worth knowing: the set.

    A set is like an array, but with two important differences. First, a set never contains duplicates — if you try to add a value that’s already there, nothing happens. Second, a set is unordered — like a dictionary, Swift makes no guarantees about sequence.

    Think about a guest list for a party. You don’t care what order the names are in, and each person can only appear once. That’s a set.

    // Creating a set — note the explicit type annotation
    var tags: Set<String> = ["swift", "ios", "mobile"]
    
    // Inserting an item
    tags.insert("apple")
    
    // Inserting a duplicate — nothing happens
    tags.insert("swift")
    print(tags.count)  // still 4, not 5
    
    // Checking membership — sets are fast at this
    print(tags.contains("ios"))    // true
    print(tags.contains("android")) // false
    
    // Removing an item
    tags.remove("mobile")
    
    // Set operations
    let setA: Set = [1, 2, 3, 4]
    let setB: Set = [3, 4, 5, 6]
    
    print(setA.union(setB))        // all items from both: {1, 2, 3, 4, 5, 6}
    print(setA.intersection(setB)) // items in both: {3, 4}
    print(setA.subtracting(setB))  // items in setA but not setB: {1, 2}
    Output
    4
    true
    false
    {1, 2, 3, 4, 5, 6}
    {3, 4}
    {1, 2}
    Sets are faster than arrays for contains: Checking whether an item exists in an array requires Swift to scan through every element one by one. A set uses a hash table internally, so .contains() is nearly instant regardless of how many items the set holds. For large collections where you only need membership checks, sets are the better tool.

    Comparison: All Three Collection Types

    FeatureArrayDictionarySet
    OrderedYes — items stay in the order you add themNo — order is not guaranteedNo — order is not guaranteed
    Allows duplicatesYes — same value can appear multiple timesKeys must be unique; values can repeatNo — duplicates are silently ignored
    How you access itemsBy integer index: array[0]By key: dict["name"]No direct access — only iterate or check membership
    Best forOrdered lists where position or sequence mattersLookup tables where you need a meaningful keyUnique membership — tags, permissions, visited IDs
    contains() speedSlow on large arrays (scans every item)Fast (hash lookup by key)Very fast (hash lookup)
    Real iOS examplesList of messages, search results, table rowsUser profile fields, JSON data, settingsTags, seen notification IDs, selected items
    Swift literal syntax[1, 2, 3]["a": 1, "b": 2]Set([1, 2, 3]) or Set<Int> = [1, 2, 3]
    Array → Set → Array Use a Set to remove duplicates from an Array
    let rawList = ["swift", "ios", "swift", "mobile", "ios"]
    let unique  = Array(Set(rawList))
    print(unique)  // ["mobile", "swift", "ios"] (order may vary)
    Converting an array to a Set and back is a quick way to remove duplicate values. The downside is that the original order is lost. If you need to deduplicate while preserving order, you’ll need a different approach.
    choosing the right tool A quick decision guide
    // Does order matter? → Array
    let messages = ["Hello", "How are you?", "Great!"]
    
    // Do you need lookup by name/key? → Dictionary
    let config: [String: Bool] = ["darkMode": true, "notifications": false]
    
    // Do you need unique values and fast membership checks? → Set
    var viewedArticleIDs: Set<Int> = [101, 204, 309]
    When you’re not sure which to use, ask: do I need order? (Array), do I need named access? (Dictionary), do I need unique values? (Set). In practice, arrays handle the majority of use cases. Dictionaries and sets come in when you need their specific properties.
    Challenge 5.5 Choose the Right Collection

    Create three separate collections for these three scenarios:

    1. A playlist of song titles in order, where the same song can appear more than once.

    2. A record of which user IDs have already seen a notification — no duplicates needed, just quick membership checks.

    3. An app settings store where you look up boolean values by a setting name like “soundEnabled”.

    For each, write the collection with at least two real items and a short comment explaining your collection type choice.

    Hint: One scenario calls for an Array, one for a Set, and one for a Dictionary. Map each scenario to the feature that matters most — order, uniqueness, or key-based lookup.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Give me five real iOS app scenarios and for each one tell me which Swift collection type you would use — Array, Dictionary, or Set — and explain why. Don’t just describe the syntax. Focus on the reasoning and tradeoffs.
    I want to make sure I understand when to use a Set vs an Array. Can you quiz me? Describe a few situations one at a time and ask me which collection type fits best. Tell me if I get one wrong and explain why.
    Build a Practice Example Study commented code to understand the why, not just the what
    Write a Swift example that uses all three collection types — Array, Dictionary, and Set — in a single realistic scenario, like modelling a user account in a social app. Add a comment on every line explaining what each collection is storing and why that type was chosen.
    Show me a Swift example where a Set’s union, intersection, and subtracting methods solve a practical problem — like finding shared interests between two users. Add inline comments explaining what each operation produces and when you’d use it in a real app.

    Stage 5 Recap: Collections

    You’ve added one of the most important tools in Swift to your toolkit. Here’s what you can now do:

    • Create arrays with literal syntax, type annotations, and the repeating initialiser
    • Use append, insert, remove, contains, first, last, count, and sorted to work with arrays
    • Iterate over arrays with for-in, enumerated(), indices, and forEach
    • Create and use dictionaries with String or other key types, including safe Optional lookups
    • Use sets to store unique values and perform union, intersection, and subtracting operations
    • Choose the right collection type for a given problem based on order, uniqueness, and lookup needs

    Collections are how apps store and work with real data. Everything from a list of messages to a user’s settings to a feed of posts is built on the patterns you just learned.

    The natural next stage is Stage 6 — Closures and Functional Methods, where you’ll learn to use map, filter, and reduce to transform collections in ways that are shorter, cleaner, and more expressive than manual loops.

    Learn Swift Stage 6: Optionals

    This is the stage most beginners find hardest — and the one that, once it clicks, makes everything else in Swift suddenly make sense.

    You’ve made it through five stages of Swift. You can write functions, loop through arrays, and build conditions. You have real foundations. Stage 6 is where Swift asks something new from you: it wants you to think carefully about the possibility that a value might not exist at all. That idea — “this thing might have a value, or it might have nothing” — is what optionals are about. Budget around 30 minutes per lesson, and make sure you do each challenge before moving on.

    By the end of Stage 6 you will understand what an optional is and why Swift uses them, how to safely unwrap them with if let and guard let, when to use the nil coalescing operator ??, why force unwrapping is dangerous, and how optional chaining lets you work with chains of optionals gracefully. These concepts appear in virtually every iOS app ever written. Once they are clear in your mind, reading real Swift code becomes dramatically easier.

    06
    Stage 6
    Optionals
    7 lessons · ~3.5 hrs
    6.1
    What is an Optional? The Concept of “Maybe”
    ⏱ 30 min Swift Optionals

    Before we look at any code, let’s talk about a box.

    Imagine you order a gift online. It arrives in a box. You shake the box — it might have something inside, or the seller might have shipped an empty box by mistake. You don’t know until you open it. That uncertainty is exactly what an optional represents in Swift.

    In most of the Swift code you have written so far, your variables always had a value. You declared var name = "Chris" and name was always a String. There was never a question of whether the value existed. It always did.

    But real-world data doesn’t work that way. Think about these situations:

    • A user fills out a form — but the middle name field is optional. Not everyone has one.
    • You ask the user to type a number, but they type “hello” instead. The conversion fails. There is no number.
    • You look up a username in a database. That username might not exist.
    • You try to find the first item in an array. The array might be empty.

    In each of those situations, a value might be there, or it might not. Swift uses a special type to represent this uncertainty: an Optional.

    An optional is like a container. The container either holds a value of the type you expect, or it holds a special placeholder called nil. nil means “nothing” — there is no value inside this container right now.

    You create an optional by adding a question mark after the type name. Here is what that looks like:

    // A regular String — it must always have a value
    var regularName: String = "Chris"
    
    // An optional String — it might have a value, or it might be nil
    var optionalName: String? = "Taylor"
    
    // Setting an optional to nil — this means "no value right now"
    var unknownName: String? = nil
    
    // You can also declare an optional without assigning anything
    // Swift automatically sets it to nil by default
    var middleName: String?
    
    print(optionalName)   // Optional("Taylor")
    print(unknownName)    // nil
    Output
    Optional(“Taylor”)
    nil

    Notice that when you print an optional that has a value, Swift shows it wrapped in Optional(...). That wrapping is Swift showing you the container, not just the value inside. You will learn how to get the value out of the container in Lesson 6.3.

    LineWhat it does
    StringA regular type. This variable will always hold a String. You cannot set it to nil.
    String?An optional String. The question mark tells Swift this variable might hold a String, or it might hold nil. Both are valid states.
    nilThe special value that means “there is nothing here”. Only optionals can be set to nil. Regular variables cannot.
    var middleName: String?Declaring an optional without assigning a value. Swift automatically sets it to nil. You don’t have to write = nil explicitly.
    Optional("Taylor")What Swift prints when an optional holds a value. The value is “wrapped” inside an Optional container. You’ll learn to unwrap it soon.
    Common confusion: Beginners sometimes see Optional("Taylor") in the console and wonder why it says that instead of just “Taylor”. The answer is that you haven’t opened the box yet — you’ve just printed the box itself. Unwrapping is the act of opening the box, and you’ll learn exactly how to do it in Lesson 6.3.
    Int? Any type can be made optional — not just String
    var age: Int? = 25       // optional Int with a value
    var score: Double? = nil   // optional Double with no value
    var isLoggedIn: Bool?      // optional Bool — starts as nil automatically
    You can add a question mark to any type in Swift to make it optional. Int?, Double?, Bool? — they all work the same way. The question mark just means “this container might be empty”.
    Int() A real-world case where optionals appear automatically
    // Int() tries to convert a String to an Int
    // It returns an optional because the conversion might fail
    let input = "42"
    let converted = Int(input)   // converted is Int? — might be nil
    
    let bad = "hello"
    let result = Int(bad)        // result is nil — "hello" is not a number
    This is one of the most common places you will encounter an optional in the wild. Int() returns an Int? because it cannot guarantee the String can be converted. If the String is not a valid number, the result is nil instead of crashing.
    Challenge 6.1
    Your First Optional Variables
    Create three optional variables: one String? for a user’s nickname (give it a value), one Int? for their age (set it to nil), and one Double? for their account balance (leave it unassigned). Print all three and observe what appears in the console. Then try setting your nickname variable to nil and print it again.
    Hint: Remember that an unassigned optional is automatically nil — you don’t need to write = nil explicitly, but you can if you want to.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain Swift optionals to me using a different real-world analogy from a gift box. Then ask me a question to check whether I understood the core idea, and tell me if my answer is correct.
    Why does Swift even need optionals? Couldn’t I just use an empty string “” instead of nil? Help me understand what problem optionals are actually solving — not just what they are.
    Build a Practice Example Get a learning-focused code example you can study line by line
    Write a short Swift example that shows 3 optional variables of different types — one with a value, one set to nil, and one left unassigned. Add a comment on every single line explaining what is happening and why. Write the comments for someone who has never seen an optional before.
    Show me 3 real-world situations in Swift where a function returns an optional instead of a regular value. For each one, explain in a comment why the optional is needed — what would go wrong if it returned a regular value instead.
    6.2
    Why Swift Forces You to Deal With Nil
    ⏱ 30 min Swift Optionals

    Here’s a question you might be asking yourself: why does Swift make such a big deal about nil? Other programming languages just let you have a null value anywhere and deal with the consequences later. Why does Swift care so much?

    The answer is crashes. Specifically, it’s avoiding the single most common crash in programming history. In many languages, if you try to use a variable that holds null (the equivalent of nil), your program crashes immediately. These crashes are called null pointer exceptions, and they have caused more broken apps, more frustrated users, and more late-night debugging sessions than almost anything else in the history of software development.

    Swift’s designers made a deliberate choice: they would make it impossible to accidentally use a nil value as if it were a real value. If you try to use an optional as though it definitely has a value, Swift will refuse to compile your code. It won’t even run. The error is caught before you ever ship the app to a user.

    This is the compiler acting as your safety net. Let’s see what Swift refuses to let you do:

    var username: String? = "christing"
    
    // This will NOT compile. Swift says: Value of optional type
    // 'String?' must be unwrapped to refer to member 'count'
    let length = username.count   // ERROR
    
    // You also can't use an optional String directly where a String is expected
    func greet(name: String) {
        print("Hello, \(name)!")
    }
    
    greet(name: username)   // ERROR — String? is not the same as String
    
    // You also can't assign nil to a regular variable
    var regularName: String = nil   // ERROR — regular types can never be nil

    Every one of those lines is a compile-time error. Swift won’t let the program run until you handle the optionals properly. That might feel annoying right now, but consider the alternative: a user is using your app, hits a nil value you didn’t expect, and the app crashes. No warning. No second chances. That’s what Swift is protecting you from.

    Think of it like a car with a manual transmission. You cannot accidentally put it in drive without deliberately going through the steps. The system is designed to make accidents harder, not easier. Optionals are Swift’s manual transmission for nil values.

    What Swift preventsWhy it matters
    username.count on an optionalIf username were nil, calling .count would have nowhere to go. In other languages this crashes the app. Swift stops you before it can happen.
    Passing String? where String is expectedFunctions that take a String assume they will always receive a value. Passing nil into them breaks that assumption. Swift refuses to let this happen silently.
    var name: String = nilA regular (non-optional) variable must always have a value. Swift enforces this by making nil impossible to assign to a regular type.
    The key insight: In Swift, String and String? are two completely different types. A String is guaranteed to always have a value. A String? might have a value, or it might have nil. Swift treats them as different, and it will not let you mix them up without explicitly handling the difference. This distinction is the entire foundation of optional safety.
    Type mismatch What Xcode’s error actually means
    // The error message Swift gives you is actually telling you exactly what to do
    
    var score: Int? = 95
    
    // This gives: "Value of optional type 'Int?' must be unwrapped"
    let doubled = score * 2   // ERROR
    
    // "must be unwrapped" means: open the box first, then use what's inside
    // The next lessons show you exactly how to do that
    When Xcode says “must be unwrapped”, it is telling you that you have an optional and you need to handle the possibility of nil before using it. Think of it as Xcode reminding you to check whether the box is empty before reaching inside. The next three lessons each show you a different way to do that.
    nil history Why this problem has a name: “The Billion Dollar Mistake”
    // Tony Hoare, who invented null references in 1965, later called it
    // "my billion-dollar mistake" — null/nil has caused so many bugs and
    // crashes across the history of software that the cost is incalculable.
    
    // Swift was designed so that nil can only exist where you explicitly
    // allow it (by using the ? suffix) and must be handled before use.
    // This turns what used to be a runtime crash into a compile-time error.
    You are not just learning a syntax rule here — you are learning a deliberate design decision that was made to prevent an entire category of bugs. Swift’s optional system is one of the features that makes it a genuinely safer language than many of its predecessors.
    Challenge 6.2
    Spot the Optional Errors
    Create a playground with these lines and intentionally cause three different optional-related compile errors. After each error appears in Xcode, read the error message carefully and write down (as a comment) what you think it is telling you. The goal isn’t to fix the errors yet — it’s to understand what Swift is objecting to and why. Create: an optional Int variable, then try to add 10 to it directly. An optional String, then try to print it with a greeting as if it were a regular String. A regular String variable, then try to set it to nil.
    Hint: You won’t be able to run the code — Xcode will show red error markers. That’s okay and expected. Read those markers carefully. They contain the clue you need.
    Deepen Your Understanding Understand the “why” before the “how”
    Can you give me a concrete example of a real app crash that would happen in a language without optional safety, then show how Swift’s type system prevents that same crash from ever reaching a user? Don’t write the fix yet — just help me feel why the problem matters.
    In Swift, String and String? are different types. Quiz me on this — give me 5 pairs of code snippets and ask me whether each one would cause a compile error. Tell me if I’m right or wrong after each answer.
    Build a Practice Example See the safety system in action with annotated examples
    Write 4 short Swift code snippets — 2 that would cause compile errors because of optional misuse, and 2 that are correct. Add a comment on every line explaining whether and why an error would occur. Make the examples realistic (like things I’d actually write in a real app), not abstract.
    Show me a realistic scenario where a function I write returns an optional, and show me what would happen if it returned a regular (non-optional) value instead. Add comments explaining the risk in the non-optional version and how the optional version protects the caller.
    6.3
    if let – Safe Unwrapping
    ⏱ 30 min Swift Optionals

    You know what an optional is. You know why Swift forces you to handle nil. Now it’s time to learn the first and most fundamental tool for dealing with optionals: if let.

    Going back to the gift box analogy: if let is the act of opening the box and checking whether something is inside. If there is something inside, you take it out and use it. If the box is empty, you handle that situation separately. Either way, you never reach into an empty box.

    Here is the basic pattern:

    var username: String? = "christing"
    
    // if let opens the box and checks what's inside
    if let name = username {
        // Inside this block, "name" is a regular String (not optional)
        // We know it has a value because we only get here if username wasn't nil
        print("Welcome back, \(name)!")
    } else {
        // We get here if username was nil
        print("No username found.")
    }
    
    // Now let's try it with a nil value
    var middleName: String? = nil
    
    if let middle = middleName {
        print("Middle name: \(middle)")
    } else {
        print("No middle name provided.")  // This runs
    }
    Output
    Welcome back, christing!
    No middle name provided.
    LineWhat it does
    if let name = usernameThis checks whether username contains a value. If it does, Swift extracts the value and stores it in a new constant called name. That constant is only available inside the curly braces that follow.
    Inside the if let blockname is a regular String here — not an optional. You can use it directly without any question marks or special handling.
    The else blockThis runs only when the optional was nil. You can handle the “no value” case here. The else is optional — you can leave it out if you don’t need to do anything special when nil.
    name vs usernameusername is still the optional — it still exists outside the block. name is the unwrapped value that only exists inside the if let block.
    Common gotcha: The unwrapped constant only exists inside the if let block. If you try to use name after the closing curly brace, Swift will tell you it doesn’t exist. This is intentional — the safety guarantee only applies inside the block where Swift verified the value existed.
    shadowing Reuse the same name for the unwrapped constant (Swift 5.7+)
    var username: String? = "christing"
    
    // Modern Swift (5.7+) lets you reuse the same name
    // instead of writing "if let name = username"
    if let username {
        print("Welcome, \(username)!")  // username is a String here
    }
    In Swift 5.7 and later you can write if let username instead of if let name = username. Inside the block, username refers to the unwrapped value, not the optional. This shorthand is very common in modern Swift code — you will see it a lot in real apps.
    multiple Unwrap several optionals at once
    var firstName: String? = "Chris"
    var lastName: String? = "Ching"
    
    // Unwrap both in one if let — only runs if BOTH have values
    if let first = firstName, let last = lastName {
        print("Full name: \(first) \(last)")
    } else {
        print("One or both names are missing.")
    }
    You can chain multiple optional bindings in a single if let by separating them with a comma. The block only runs if all of the optionals have values. If any one of them is nil, the else branch runs instead. This is cleaner than nesting multiple if let blocks inside each other.
    where Add an extra condition alongside the unwrap
    var age: Int? = 17
    
    // Unwrap AND check a condition in one step
    if let userAge = age, userAge >= 18 {
        print("Access granted.")
    } else {
        print("Access denied.")  // Runs — age is 17
    }
    After the optional binding, you can add a comma and then any boolean condition. The block only runs if the optional has a value AND the condition is true. This is much cleaner than nesting an if inside an if let.
    Int() + if let The most common pattern you’ll use in real apps
    // User typed something — we don't know if it's a valid number
    let userInput = "42"
    
    if let number = Int(userInput) {
        // userInput was a valid number — number is an Int here
        print("You entered: \(number)")
        print("Doubled: \(number * 2)")
    } else {
        print("That wasn't a valid number.")
    }
    This is exactly the pattern you will use when accepting input from a user in a real app. Int(userInput) returns Int? — an optional that is nil if the conversion fails. Wrapping it in if let lets you handle both outcomes cleanly.
    Challenge 6.3
    The User Profile Unwrap
    Create three optional variables: displayName: String?, followersCount: Int?, and bio: String?. Assign a value to displayName and followersCount, but leave bio as nil. Use a single if let with multiple bindings to unwrap displayName and followersCount together and print a profile summary. Then use a separate if let for bio — and in its else branch, print “No bio yet.” Finally, try the modern shorthand syntax (if let displayName) on one of your variables.
    Hint: Chaining multiple bindings in one if let uses commas to separate them. All of them must have values for the block to run.
    Deepen Your Understanding Solidify the concept through explanation and quizzing
    Explain what “unwrapping” means in the context of Swift optionals. Use the gift box analogy if it helps, but also give me a different way of thinking about it. Then quiz me with 3 short questions to make sure I understand.
    What’s the difference between “if let name = username” and “if let username” in Swift? When would I use each one? Ask me a follow-up question to check my understanding after you explain.
    Audit Your Own Code Get targeted feedback on your challenge solution
    Here’s my Swift code from Challenge 6.3: [paste your code]. Did I use if let correctly? Are there any places where I could simplify the unwrapping or handle nil more elegantly? Explain your reasoning before suggesting any changes.
    Review this Swift code for how I’m using if let: [paste your code]. Is there anything unsafe about how I’m handling the optionals? If I ran this with nil values in unexpected places, would anything break? Explain before suggesting fixes.
    6.4
    guard let – The Pattern You’ll Use Most in Real Apps
    ⏱ 30 min Swift Optionals

    if let is great when you want to do something inside a block if a value exists. But there is a different situation that comes up constantly in real iOS apps: you are at the start of a function, and if the optional doesn’t have a value, the whole function should stop. There is nothing useful to do without that value.

    Imagine a function that loads a user’s profile from a server. If there is no user ID, there is no point in making the network request. You want to bail out early. That’s what guard let is designed for.

    Here is a good way to think about it: if let says “if the value exists, do something with it”. guard let says “the value must exist — if it doesn’t, stop everything right now”.

    func loadProfile(userID: String?) {
    
        // guard let: if userID is nil, we exit the function immediately
        // The "else" block MUST exit — return, throw, or break
        guard let id = userID else {
            print("No user ID — cannot load profile.")
            return   // Exit the function
        }
    
        // If we reach this line, id is a regular String — guaranteed
        // Notice that id is available for the REST of the function
        // (unlike if let, where it was only available inside the block)
        print("Loading profile for user: \(id)")
        print("Profile loaded successfully.")
    }
    
    loadProfile(userID: "user_12345")   // Loads profile
    loadProfile(userID: nil)              // Prints error, returns early
    Output
    Loading profile for user: user_12345
    Profile loaded successfully.
    No user ID — cannot load profile.
    LineWhat it does
    guard let id = userID else {Check whether userID has a value. If it does, extract it into id and continue. If userID is nil, run the else block.
    Inside the else blockThis is the failure case — what to do when the optional is nil. This block MUST exit the current scope. You have to write return, break, continue, or throw. Swift enforces this at compile time.
    id after the guard blockThis is the big difference from if let. With guard let, the unwrapped constant is available for the entire rest of the function, not just inside a block.
    The rule you cannot break: The else block in a guard statement must exit the current scope. You are not allowed to just print a message and keep going — Swift will refuse to compile. This is intentional: guard is designed as an early exit tool. If you want to keep going even when the value is nil, use if let instead.
    if let vs guard let Choosing the right tool for the situation
    // Use if let when you want to do something IF the value exists
    // The unwrapped value is only available inside the block
    if let nickname = user.nickname {
        print("Hi \(nickname)!")   // nickname exists here
    }
    // nickname does NOT exist here
    
    // Use guard let when the function cannot proceed without the value
    // The unwrapped value is available for the rest of the function
    func processOrder(orderID: String?) {
        guard let id = orderID else { return }
        // id is available for the entire rest of the function
        print("Processing order: \(id)")
    }
    A useful rule of thumb: if you need the unwrapped value for the rest of the function, use guard let. If you only need it for one short thing inside a block, use if let. In practice, guard let is the pattern you will see most often in real iOS code because functions usually need their inputs to be valid to be useful at all.
    multiple guard Unwrap several optionals at the start of a function
    func sendMessage(from sender: String?, to recipient: String?, body: String?) {
    
        // Unwrap all required inputs at once
        guard let from = sender,
              let to = recipient,
              let message = body else {
            print("Missing required fields.")
            return
        }
    
        // All three are available and guaranteed to have values here
        print("\(from) → \(to): \(message)")
    }
    You can chain multiple bindings in a single guard let using newlines. If any one of them is nil, the else block runs. This pattern at the top of a function is sometimes called a “guard clause” and it keeps the main logic of your function clean by handling all the failure cases upfront.
    Challenge 6.4
    Guard the Login Function
    Write a function called attemptLogin that takes three optional parameters: email: String?, password: String?, and deviceID: String?. Use a single guard let to unwrap all three at once. If any are nil, print a specific error message and return early. If all three have values, print a success message that includes all three values. Then call the function three times: once with all values, once with email set to nil, and once with all three set to nil.
    Hint: Chain the three bindings in a single guard statement using commas (or newlines for readability). They all share the single else block.
    Deepen Your Understanding Understand when to choose guard let over if let
    Can you give me 3 realistic examples of iOS app code where guard let is clearly the better choice over if let, and 2 examples where if let is clearly better? For each example, explain why in plain English — what goes wrong if you use the other one.
    I’m trying to understand why guard let requires an exit in its else block — why can’t you just continue on if the value is nil? Help me understand the design intention behind this rule, not just what the rule is.
    Audit Your Own Code Check whether you picked the right unwrapping tool
    Here’s my Swift function from Challenge 6.4: [paste your code]. Did I use guard let in the right place? Are there any cases I’m not handling correctly? Explain before suggesting changes.
    Look at this Swift function I wrote: [paste your code]. For each optional I’m unwrapping, tell me whether if let or guard let would be more appropriate and why. Don’t rewrite the code — just evaluate my choices.
    6.5
    The nil Coalescing Operator (??)
    ⏱ 30 min Swift Optionals

    Sometimes you don’t need to do anything special when a value is nil. You just want a sensible default to fall back on. You want to say: “Use this value if it exists, otherwise use this other value instead.”

    Swift has a very clean way to express that idea: the nil coalescing operator, written as two question marks: ??.

    Think of it like a backup plan. You ask your first choice (the optional), and if that’s not available, you fall back to your second choice (the default value). The result is always a regular, non-optional value — so you can use it directly without any more unwrapping.

    var nickname: String? = nil
    let displayName = nickname ?? "Anonymous"
    print(displayName)   // Anonymous
    
    var savedVolume: Int? = 75
    let volume = savedVolume ?? 50
    print(volume)   // 75 — savedVolume had a value so the default wasn't used
    
    var profileColor: String? = nil
    print("Theme color: \(profileColor ?? "blue")")   // Theme color: blue
    Output
    Anonymous
    75
    Theme color: blue
    ExpressionWhat it does
    nickname ?? "Anonymous"If nickname has a value, use it. If nickname is nil, use “Anonymous” instead. The result is always a String, never an optional.
    savedVolume ?? 50savedVolume was 75, so 75 is used. The default (50) is ignored because the optional had a value.
    Using ?? inside string interpolationYou can use ?? anywhere you need a non-optional value, including inside \() in a string. This avoids printing “Optional(…)” in your output.
    Type must match: The value on the right side of ?? must be the same type as the value inside the optional. You cannot write someString ?? 0 — a String optional needs a String default, and an Int optional needs an Int default. Swift will give you a type error if they don’t match.
    chaining ?? Multiple fallbacks in sequence
    var preferredName: String? = nil
    var username: String? = nil
    
    // Try preferredName first, then username, then fall back to "Guest"
    let displayName = preferredName ?? username ?? "Guest"
    print(displayName)   // Guest — both optionals were nil
    You can chain multiple ?? operators. Swift evaluates them left to right and uses the first non-nil value it finds. If all of them are nil, it falls back to the final default on the right. This is a clean way to express a priority list of fallbacks.
    ?? in print Avoid “Optional(…)” in console output
    var city: String? = nil
    
    // Without ??: prints "Optional(nil)" or "nil"
    print(city)                       // nil
    
    // With ??: always prints a clean String
    print(city ?? "Unknown city")   // Unknown city
    
    // Also useful inside string interpolation
    print("Location: \(city ?? "Not set")")   // Location: Not set
    Using ?? is a clean way to provide user-visible fallback text without writing a full if let block. It’s especially common in SwiftUI views where you want to display something meaningful even when optional data hasn’t loaded yet.
    Challenge 6.5
    Settings with Defaults
    Imagine an app where user settings are stored as optionals (because not all users have customized them). Create these optional variables: preferredLanguage: String? (nil), fontSize: Int? (nil), darkMode: Bool? (set to true), and username: String? (nil). Use ?? to resolve each one to a final value with sensible defaults. Print a settings summary that shows all four resolved values. Then add a second optional displayName: String? that is also nil, and chain it with username using ?? so that the app first tries displayName, then username, then falls back to “Guest”.
    Hint: Remember the type on the right of ?? must match the type of the optional. For Bool? your default should be a Bool (not a String).
    Deepen Your Understanding Understand when ?? is the right choice
    When should I use ?? instead of if let or guard let? Help me understand the decision: what kind of situation calls for ?? and what kind calls for the others? Give me 3 concrete examples of each.
    Can I use ?? to provide a default and then still call methods on the result? Walk me through how that works with an example, and explain why the result of ?? is always non-optional.
    Build a Practice Example See ?? used in realistic app code
    Write a short Swift example of a SwiftUI-style view model that uses ?? in at least 4 places to provide display values from optional data. Add a comment on every line explaining what the ?? is doing and why a default was chosen. Write the comments for a beginner.
    Give me 3 examples of chained ?? operators in realistic Swift code — situations with priority-ordered fallbacks. Explain in comments why each fallback exists and what would happen if the chain weren’t there.
    6.6
    Force Unwrapping (!) – What It Is and Why to Avoid It
    ⏱ 30 min Swift Optionals

    Every optional tool you have learned so far — if let, guard let, ?? — deals with nil carefully. They check whether a value exists before using it. Swift provides one more way to get the value out of an optional, but this one is different. It doesn’t check. It just grabs.

    Force unwrapping uses an exclamation mark after an optional to say: “I’m certain this optional has a value. Skip the check and give me the value right now.” It looks like this: username!

    The problem is: if you are wrong — if the optional is actually nil — your app crashes immediately. Not an error message. Not a warning. A hard crash that kills the app right in front of your user.

    var name: String? = "Chris"
    
    // Force unwrap — works here because name actually has a value
    print(name!)   // Chris
    
    // Now change name to nil and force unwrap again...
    name = nil
    print(name!)   // CRASH: Fatal error: Unexpectedly found nil while unwrapping
    Output
    Chris
    Fatal error: Unexpectedly found nil while unwrapping an Optional value
    LineWhat it does
    name!The exclamation mark says “I guarantee this optional has a value — give it to me directly”. Swift trusts you and doesn’t check. If you’re right, you get the value. If you’re wrong, the app crashes.
    “Fatal error: Unexpectedly found nil”This is the crash message you see when you force unwrap a nil optional. You will see this message in the Xcode console. It means someone used ! on an optional that turned out to be nil.
    Strong warning: Force unwrapping is not a useful shortcut for beginners — it is a source of crashes. Every time you write ! on an optional, you are making a promise to Swift that you cannot always keep. If the data changes, if a network request fails, if a user provides unexpected input, that promise breaks and your app crashes. Use if let, guard let, or ?? instead. They will never crash you.
    Crash in production means: Your user is using your published app. They hit a screen. The app suddenly closes with no explanation. They leave a one-star review. That is what force unwrapping on nil looks like in the real world. The crash is instant and gives the user no chance to recover. This is exactly the class of bug that Swift’s optional system was designed to prevent — and force unwrapping turns that protection off completely.

    So when, if ever, is force unwrapping acceptable?

    There is a small set of situations where experienced developers use it deliberately. For example, when loading an image that is bundled with the app — you are certain the file exists because you put it there. Or when working with @IBOutlet properties that are connected in Interface Builder. In those cases, the developer has external knowledge that the value will never be nil.

    As a beginner, you do not yet have the experience to know when those situations arise. The safe rule is: never use ! unless you can explain exactly why nil is impossible in that specific case, and you have verified it is impossible in all code paths.

    IUO Implicitly unwrapped optionals — a special case
    // An implicitly unwrapped optional is declared with ! instead of ?
    // It is treated as optional for storage, but as non-optional for use
    var label: UILabel!   // Implicitly unwrapped — common in IBOutlets
    
    // You can use it without unwrapping syntax
    // But if it's nil when you access it, the app still crashes
    // label.text = "Hello"   // Crash if label is nil
    
    // You mostly see this in generated Xcode code, not code you write yourself
    You will see ! in type declarations (not just after a variable name) in auto-generated Xcode code, especially with @IBOutlet. These are called implicitly unwrapped optionals. They are optionals that behave like non-optionals. You don’t need to write these yourself as a beginner — just know they exist and that they carry the same crash risk as force unwrapping.
    safe alternative Replace ! with if let or ?? in almost every case
    var username: String? = nil
    
    // AVOID: force unwrap — crashes if username is nil
    // let name = username!   ← don't do this
    
    // PREFER: if let — handles nil safely
    if let name = username {
        print(name)
    }
    
    // PREFER: ?? with a default — always safe
    let name = username ?? "Guest"
    print(name)
    In almost every situation where a beginner reaches for !, one of the safe alternatives is the better choice. If you find yourself typing ! to make an error go away, that is the signal to use if let or ?? instead.
    Challenge 6.6
    Rewrite the Unsafe Code
    You have been given this (intentionally dangerous) code that uses force unwrapping: var score: Int? = nil, let result = score! * 2, print(result). Your job is to rewrite it three times: once using if let, once using guard let inside a function, and once using ?? with a default of 0. Each version should handle the nil case without crashing. Add a comment to each version explaining why it is safer than the original.
    Hint: For the guard let version you’ll need to wrap the code in a function since guard requires an exit route.
    Deepen Your Understanding Understand the real-world cost of force unwrapping
    Can you show me a realistic scenario where a beginner might think force unwrapping is safe, but a small change in the app’s state would cause a crash? Walk me through exactly what the crash looks like and why it happened.
    Are there any situations where force unwrapping is genuinely acceptable and professional iOS developers use it deliberately? Help me understand what those situations are and what makes them different from careless force unwrapping.
    Audit Your Own Code Find and eliminate dangerous force unwraps
    Here’s some Swift code I wrote: [paste your code]. Can you find any force unwraps (!) that might be dangerous? For each one, explain why it’s risky and suggest the safest replacement using if let, guard let, or ??.
    Review my rewritten versions from Challenge 6.6: [paste your three versions]. For each version, tell me whether the nil handling is correct and complete. Are there any edge cases I’m missing? Explain before suggesting any changes.
    6.7
    Optional Chaining
    ⏱ 30 min Swift Optionals

    You have learned how to safely unwrap a single optional. But what about situations where you have a chain of optionals? Where to get to the thing you want, you have to go through several values that might each be nil?

    Imagine a user profile. The user might not be logged in. If they are logged in, they might not have set an address. If they have an address, it might not have a postal code. To get the postal code, you have to get through three levels of “maybe”.

    Without optional chaining, you would need a nested stack of if let statements — one inside the other, inside the other. It gets messy fast. Optional chaining gives you a cleaner way to navigate that chain using the ?. syntax.

    // Set up some simple structs to model the data
    struct Address {
        var street: String
        var postalCode: String
    }
    
    struct UserProfile {
        var name: String
        var address: Address?   // Address is optional — not all users have one
    }
    
    struct App {
        var currentUser: UserProfile?   // User is optional — might not be logged in
    }
    
    var app = App(currentUser: nil)
    
    // Optional chaining with ?.  — each ? means "if this exists, keep going"
    // If any part of the chain is nil, the whole expression returns nil
    let postalCode = app.currentUser?.address?.postalCode
    
    print(postalCode ?? "No postal code available")
    // No postal code available — because currentUser was nil
    
    // Now let's give the app a logged-in user with an address
    let address = Address(street: "100 Main St", postalCode: "M5V 1A1")
    let user = UserProfile(name: "Chris", address: address)
    app.currentUser = user
    
    let code = app.currentUser?.address?.postalCode
    print(code ?? "No postal code available")
    // M5V 1A1 — all parts of the chain had values
    Output
    No postal code available
    M5V 1A1
    LineWhat it does
    app.currentUser?.address?.postalCodeRead left to right: “Get currentUser — if it exists, get its address — if that exists, get its postalCode”. Each ?. is a safe step that stops and returns nil if the value before it is nil.
    The result type of ?.The result of any optional chaining expression is always an optional. Even though postalCode is a regular String inside the struct, accessing it through optional chaining makes the result String?. This is why pairing it with ?? is so common.
    If any link in the chain is nilThe entire expression short-circuits and returns nil immediately. No crash. No need to unwrap each step manually. The chain just stops and gives you nil.
    Result is always optional: When you use ?., even once, the result of the whole expression is optional — regardless of what property you are accessing at the end. This is why app.currentUser?.name gives you String? even though UserProfile.name is a regular String. The chain could have stopped at any point, so the result must be optional.
    method chaining Call methods on optional values with ?.method()
    var optionalText: String? = "hello world"
    
    // Call a method on an optional using optional chaining
    let uppercased = optionalText?.uppercased()
    print(uppercased ?? "no text")   // HELLO WORLD
    
    var nilText: String? = nil
    let result = nilText?.uppercased()
    print(result ?? "no text")         // no text — nilText was nil
    Optional chaining works with method calls too, not just property access. If the optional is nil, the method is never called and the result is nil. If the optional has a value, the method is called and its return value is wrapped in an optional.
    ?. + if let Combine optional chaining with if let for a final unwrap
    struct Company {
        var ceo: String?
    }
    struct Employee {
        var company: Company?
    }
    
    var employee = Employee(company: Company(ceo: "Chris"))
    
    // Use optional chaining to navigate the chain, then unwrap the final result
    if let ceoName = employee.company?.ceo {
        print("CEO: \(ceoName)")   // CEO: Chris
    } else {
        print("No CEO found")
    }
    Pairing ?. chaining with if let is extremely common. Use the chain to navigate to the value you want, then use if let to unwrap the final optional result into a usable value. The chain handles the “path”, and if let handles the “destination”.
    Challenge 6.7
    The Deep Profile Chain
    Create three structs: Subscription with a plan: String property, Account with an optional subscription: Subscription?, and User with an optional account: Account?. Then create a User variable and try three scenarios: user with no account (both account and subscription are nil), user with an account but no subscription, and user with an account and a subscription. For each scenario, use optional chaining to safely access the plan name and print either the plan or “No active plan” using ??.
    Hint: You’ll need to access the chain like user.account?.subscription?.plan. This returns String? — pair it with ?? to resolve to a display value.
    Deepen Your Understanding Understand optional chaining’s short-circuit behavior
    Can you explain exactly what “short-circuiting” means when it comes to optional chaining in Swift? What happens step by step when a nil is encountered midway through a chain like user?.profile?.settings?.theme? Walk me through it as if you’re describing how the Swift runtime actually processes it.
    What’s the difference between optional chaining and just nesting multiple if let statements? When would I use one versus the other? Give me a side-by-side example of both approaches for the same data to help me see the difference.
    Build a Practice Example See optional chaining in realistic iOS app code
    Write a realistic Swift example modeling an e-commerce app with at least 3 levels of optional nesting (for example: Cart -> Item -> ProductDetails -> ImageURL). Use optional chaining to safely access a deeply nested value, and add a comment on every line explaining what each ?. is doing and why nil is possible at that step.
    Give me 3 different examples of optional chaining with method calls (not just property access) in realistic Swift code. Add comments explaining why the method might not be called at all, and what value is returned in that case.

    Stage 6 Recap: Optionals

    This was the hardest stage in the curriculum — and you made it through. That matters. Optionals are the concept that separates people who give up on Swift from people who break through. You are now in the second group.

    • An optional is a container that either holds a value or holds nil — the concept of “maybe a value, maybe nothing”
    • Swift prevents you from using an optional as though it definitely has a value — this is intentional safety, not an inconvenience
    • if let safely unwraps an optional into a new constant that is only available inside the block
    • guard let safely unwraps an optional at the top of a function and exits early if nil — the unwrapped value is available for the whole function
    • ?? provides a default value when an optional is nil — the result is always non-optional
    • Force unwrapping (!) skips the nil check — if the optional is nil, the app crashes immediately. Avoid it as a beginner.
    • Optional chaining (?.) lets you navigate through chains of optionals safely — if any link is nil, the whole chain returns nil without crashing

    Something important: now that optionals make sense to you, you will start seeing them differently in real Swift code. Code that used to look confusing — all those question marks and exclamation marks scattered through Apple’s examples — will start making sense. That is not a small thing.

    Stage 7 covers closures and higher-order functions — the tools that let you write concise, powerful code using map, filter, reduce, and custom callbacks. With your optional foundations solid, you are ready for it.

    Learn Swift Stage 7: Structs and Classes

    This is the stage where everything clicks — you’ll learn how to bundle data and behaviour together into your own custom types, which is exactly how real Swift apps are built.

    To follow along, open Xcode and create a new Swift Playground (File → New → Playground). Each lesson takes roughly 25–30 minutes including the challenge. Set aside about 2.5 hours for the full stage, or work through one lesson at a time.

    By the end of Stage 7 you will know what a struct is and how to define one, how to give it properties and methods, how initializers work, why structs and classes behave differently, and how to use mutating methods. After this stage you are ready to move into SwiftUI and start building real app screens.

    07
    Stage 7
    Structs and Classes
    5 lessons · ~2.5 hrs
    7.1
    What Is a Struct?
    ⏱ 30 min Swift Structs and Classes

    So far, the types you have worked with — String, Int, Bool — are all built into Swift. But what happens when you need to represent something more specific, like a user in your app, or a recipe, or a product in a store?

    That is exactly what a struct is for. A struct lets you create your own custom data type by grouping related pieces of data together under one name.

    Think of a struct like a blueprint. A blueprint for a house describes everything the house will have: how many rooms, what colour the walls are, whether there is a garage. The blueprint itself is not a house — but you can use it to create as many houses as you want, and each house can have its own values.

    In the same way, a struct is a blueprint. You define it once, then use it to create as many instances as you need — each with its own data.

    // Define a struct — this is the blueprint
    struct Contact {
        var name: String       // stored property: the contact's name
        var email: String      // stored property: the contact's email
        var age: Int           // stored property: the contact's age
    }
    
    // Create an instance — this is a real contact made from the blueprint
    var alice = Contact(name: "Alice", email: "alice@example.com", age: 28)
    
    // Access individual properties with dot syntax
    print(alice.name)   // Alice
    print(alice.email)  // alice@example.com
    print(alice.age)    // 28
    
    // Create a second instance — completely separate from alice
    var bob = Contact(name: "Bob", email: "bob@example.com", age: 34)
    print(bob.name)     // Bob
    Output
    Alice
    alice@example.com
    28
    Bob
    LineWhat it does
    struct Contact { }Defines a new custom type called Contact. The word struct signals we are creating a blueprint.
    var name: StringDeclares a stored property. Every Contact instance will have a name of type String.
    Contact(name:email:age:)Creates a new instance of the struct. Swift automatically provides this initializer from the properties.
    alice.nameDot syntax — accesses the name property on the alice instance specifically.
    var bob = Contact(...)Creates a completely separate instance. Changing bob’s data does not affect alice at all.
    Naming convention: Struct names always start with a capital letter — Contact, not contact. This is how you tell the difference between a type and a variable at a glance. Property names start with a lowercase letter, just like regular variables.
    struct with let properties Use let when a property should never change after creation
    struct Product {
        let id: String          // cannot be changed after creation
        var name: String        // can be updated
        var price: Double       // can be updated
    }
    
    var item = Product(id: "SKU-001", name: "Notebook", price: 12.99)
    item.name = "Premium Notebook"  // fine — name is var
    // item.id = "SKU-002"           // error — id is let
    Inside a struct, use let for properties that identify or define the item and should never change, and var for properties that are allowed to be updated later.
    nested struct usage A struct can have another struct as one of its properties
    struct Address {
        var city: String
        var country: String
    }
    
    struct User {
        var name: String
        var address: Address  // property whose type is another struct
    }
    
    let user = User(
        name: "Maria",
        address: Address(city: "Toronto", country: "Canada")
    )
    print(user.address.city)  // Toronto
    Structs can be composed — one struct can hold another struct as a property. This lets you model complex real-world objects cleanly without putting everything in one giant struct.
    Challenge 7.1 — Model a Movie

    Create a struct called Movie with these properties: a title (String), a director (String), a releaseYear (Int), and an isAvailable Bool that tracks whether the movie is currently streaming.

    Create two different Movie instances. Print the title and availability of each one. Then change the isAvailable value on one of them and print again to confirm it updated.

    Hint: Use dot syntax to read and update properties after creation — for example myMovie.isAvailable = true. Remember isAvailable needs to be declared with var so it can change.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain Swift structs to me like I’ve never seen one before. Use a real-world analogy, then show me the simplest possible example and walk through every line.
    What is the difference between defining a struct and creating an instance of a struct? Explain in plain English — no code yet.
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift struct for a recipe app. Give it at least four properties. Add a comment on every single line explaining what it does and why — write the comments as if explaining to a complete beginner.
    Give me three different examples of Swift structs I’d encounter in a real iOS app — a social media app, a fitness tracker, and a shopping app. For each one, explain why you chose those properties.
    7.2
    Properties and Methods
    ⏱ 30 min Swift Structs and Classes

    A struct does not just hold data — it can also do things. The data inside a struct is called properties. The actions a struct can perform are called methods. Together, they give your custom type both a description and a set of behaviours.

    If properties are the nouns of a struct — the things it has — then methods are the verbs. Think of a bank account: it has a balance and an owner (properties), but it can also deposit money, withdraw money, and display a summary (methods).

    struct BankAccount {
    
        // Properties — the data this struct holds
        let owner: String
        var balance: Double
    
        // Computed property — calculated from other properties, not stored
        var formattedBalance: String {
            "$\(balance)"               // returns a formatted string
        }
    
        // Method — a function defined inside the struct
        func printSummary() {
            print("\(owner)'s balance: \(formattedBalance)")
        }
    
        // Method that takes a parameter
        func canAfford(amount: Double) -> Bool {
            return balance >= amount          // returns true if balance covers the amount
        }
    }
    
    var account = BankAccount(owner: "Sam", balance: 500.0)
    account.printSummary()              // call a method with dot syntax
    print(account.canAfford(amount: 200.0))  // true
    print(account.canAfford(amount: 750.0))  // false
    print(account.formattedBalance)           // $500.0
    Output
    Sam’s balance: $500.0
    true
    false
    $500.0
    LineWhat it does
    var balance: DoubleA stored property — its value is saved in memory and can be read or changed.
    var formattedBalance: String { ... }A computed property — it looks like a property but is calculated each time it is accessed. No value is stored.
    func printSummary()A method — a function that belongs to the struct. It can access the struct’s properties using their names directly.
    func canAfford(amount:) -> BoolA method that takes a parameter and returns a value. Methods work exactly like regular functions — they just live inside a struct.
    account.printSummary()Calling a method uses the same dot syntax as accessing a property — just add parentheses at the end.
    Computed properties vs methods: Use a computed property when the result is a single value derived from the struct’s data. Use a method when you need parameters, side effects (like printing), or more complex logic. If it feels like a question — “what is the formatted balance?” — it is usually a computed property.
    computed property Derive a value from stored properties — no stored value needed
    struct Rectangle {
        var width: Double
        var height: Double
    
        // computed — calculated from width and height, not stored
        var area: Double {
            width * height
        }
    
        var perimeter: Double {
            2 * (width + height)
        }
    }
    
    let rect = Rectangle(width: 10, height: 5)
    print(rect.area)       // 50.0
    print(rect.perimeter)  // 30.0
    Computed properties keep structs clean — you never have to remember to update area when width changes, because it is always calculated on the fly.
    method with return value Methods can accept parameters and return any type
    struct TemperatureConverter {
        var celsius: Double
    
        func toFahrenheit() -> Double {
            (celsius * 9 / 5) + 32
        }
    
        func isFreezing() -> Bool {
            celsius <= 0
        }
    }
    
    let temp = TemperatureConverter(celsius: 22)
    print(temp.toFahrenheit())  // 71.6
    print(temp.isFreezing())   // false
    Methods are just functions that live inside the struct. They can access all the struct’s properties directly by name — no need to pass them in as parameters.
    Challenge 7.2 — Build a Podcast Tracker

    Create a struct called Podcast with stored properties for title (String), episodeCount (Int), and averageMinutesPerEpisode (Int).

    Add a computed property called totalHours that returns the total listening time in hours as a Double. Add a method called printDescription() that prints a friendly sentence like “The Daily has 300 episodes — about 150.0 hours of content.”

    Hint: Total minutes = episodeCount * averageMinutesPerEpisode. Divide by 60 to get hours — remember to convert to Double first so you get a decimal result. Inside your method, access the computed property the same way you would from outside: just use its name.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    What is the difference between a stored property and a computed property in a Swift struct? Give me a real-world analogy for each, then explain when I should use one over the other.
    Inside a Swift struct method, how does the method know which instance’s data to use? Walk me through what happens step by step when I call a method on an instance.
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift struct for a workout tracking app. Give it at least two stored properties, one computed property, and two methods. Add a comment on every line explaining what it does and why — aimed at a beginner.
    Show me the same struct written three ways: one with only stored properties, one with a computed property added, and one with methods added. Walk me through what changed each time and why.
    7.3
    Initializers
    ⏱ 30 min Swift Structs and Classes

    Every time you create an instance of a struct, Swift needs to set up all of its properties before you can use it. The code that does that setup is called an initializer.

    You have already been using one without thinking about it. When you write Contact(name: "Alice", email: "alice@example.com", age: 28), that is calling an initializer. Swift generated it automatically from your properties. This is called the memberwise initializer, and you get it for free with every struct.

    But sometimes you want more control over how instances are created. Maybe some properties have sensible defaults. Maybe you want to accept simpler inputs and do some calculation before setting a property. That is when you write a custom initializer.

    struct AppUser {
        var username: String
        var email: String
        var isPremium: Bool
        var joinedMessage: String
    
        // Custom initializer — we control what parameters are required
        init(username: String, email: String) {
            self.username = username        // self.username = the property, username = the parameter
            self.email = email
            self.isPremium = false           // default value — new users are never premium
            self.joinedMessage = "Welcome, \(username)!"  // calculated from the username
        }
    }
    
    // Notice: we only pass username and email — isPremium and joinedMessage are set automatically
    let user = AppUser(username: "sarah_codes", email: "sarah@example.com")
    print(user.joinedMessage)  // Welcome, sarah_codes!
    print(user.isPremium)      // false
    Output
    Welcome, sarah_codes!
    false
    LineWhat it does
    init(username:email:)Declares a custom initializer. The keyword is init — no func needed, and no return type.
    self.username = usernameself refers to the current instance being created. It distinguishes the property (self.username) from the parameter (username) when they share the same name.
    self.isPremium = falseSets a default value inside the initializer. The caller does not need to provide this — the initializer handles it.
    self.joinedMessage = "Welcome, \(username)!"You can do work inside an initializer before setting a property — here, building a greeting string from the username parameter.
    Important: Once you write a custom initializer, Swift removes the automatic memberwise initializer. If you want both, you can keep the default one by adding your custom initializer in an extension instead. For now, just know that if you define an init, you are taking over that job from Swift.
    default property values Set defaults inline so callers can omit them
    struct NotificationSettings {
        var soundEnabled: Bool = true    // default value right on the property
        var badgeEnabled: Bool = true
        var alertStyle: String = "banner"
    }
    
    // No arguments needed — all defaults apply
    let defaults = NotificationSettings()
    
    // Override just the ones you need
    let custom = NotificationSettings(soundEnabled: false, badgeEnabled: true, alertStyle: "alert")
    print(defaults.alertStyle)  // banner
    print(custom.alertStyle)    // alert
    When you assign default values directly on properties, Swift keeps the memberwise initializer and makes those parameters optional. This is often cleaner than writing a custom init just to set defaults.
    multiple initializers A struct can have more than one init — Swift picks based on the arguments
    struct Color {
        var red: Double
        var green: Double
        var blue: Double
    
        // Init from individual RGB values
        init(red: Double, green: Double, blue: Double) {
            self.red = red
            self.green = green
            self.blue = blue
        }
    
        // Convenience init for white (all channels equal)
        init(white: Double) {
            self.red = white
            self.green = white
            self.blue = white
        }
    }
    
    let red = Color(red: 1.0, green: 0.0, blue: 0.0)
    let gray = Color(white: 0.5)
    Swift identifies which initializer to use by matching the argument labels. As long as each init has a different signature (different parameter names or types), you can have as many as you need.
    Challenge 7.3 — Smart Event Creator

    Create a struct called CalendarEvent with properties: title (String), durationMinutes (Int), isAllDay (Bool), and reminder (String).

    Write a custom initializer that takes only title and durationMinutes. Inside the initializer, set isAllDay to false by default. Set reminder to a string like "Reminder: 15 minutes before [title]" so it is automatically generated from the title.

    Hint: Use self.reminder = "Reminder: 15 minutes before \(title)" — you can use the parameter value in string interpolation inside the initializer before the property is set. Make sure you set every property before the initializer ends, or Swift will give you an error.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain what self means inside a Swift initializer. Why do we need it, and when can we leave it out? Use a simple analogy before showing any code.
    What is the memberwise initializer in Swift? Where does it come from, and what happens to it when I write a custom init? Explain this without writing any code first.
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift struct for a messaging app that has a custom initializer setting some properties automatically. Add a comment on every single line explaining what it does and why — aimed at a complete beginner.
    Show me a Swift struct with two different initializers. Walk me through step by step what Swift does when each one is called — which properties get set, in what order, and why.
    7.4
    Value Types vs Reference Types
    ⏱ 30 min Swift Structs and Classes

    This is one of the most important concepts in Swift — and one that catches a lot of beginners off guard. The key question is: when you copy a piece of data and change the copy, does the original change too?

    The answer depends on whether you are working with a value type (like a struct) or a reference type (like a class).

    Think of it like this. Imagine you have a recipe written on a piece of paper. If you photocopy it and hand the copy to a friend who scribbles on it, your original is untouched — they changed their copy, not yours. That is a value type. Now imagine you and a friend are both looking at the same document on Google Docs. If they change it, you see the change too — you are both working with the same thing. That is a reference type.

    // STRUCT — value type (each copy is independent)
    struct PointStruct {
        var x: Int
        var y: Int
    }
    
    var pointA = PointStruct(x: 1, y: 2)
    var pointB = pointA      // Swift makes a full copy of pointA
    pointB.x = 99           // changing pointB does NOT affect pointA
    
    print(pointA.x)  // 1 — unchanged
    print(pointB.x)  // 99 — only pointB changed
    
    // ─────────────────────────────────────────────
    
    // CLASS — reference type (copies share the same object)
    class PointClass {
        var x: Int
        var y: Int
        init(x: Int, y: Int) {
            self.x = x
            self.y = y
        }
    }
    
    var classA = PointClass(x: 1, y: 2)
    var classB = classA      // classB points to the SAME object as classA
    classB.x = 99           // changing classB ALSO changes classA
    
    print(classA.x)  // 99 — classA changed too!
    print(classB.x)  // 99
    Output
    1
    99
    99
    99
    ConceptWhat it means
    var pointB = pointA (struct)Swift creates a completely independent copy. pointA and pointB are two separate pieces of data in memory.
    var classB = classA (class)Swift does not copy — it creates a second reference pointing at the same object. Both variables share one object in memory.
    Value typeStructs, enums, and all Swift basic types (String, Int, Array, etc.) are value types. Assignment always copies.
    Reference typeClasses are reference types. Assignment shares the same object. You only get a copy if you explicitly write code to create one.
    SwiftUI note: SwiftUI views are structs. This is why SwiftUI can compare an old view to a new one and know what changed — structs are copied, not shared. Understanding this distinction is essential once you start building with SwiftUI.
    struct (value type) Each variable gets its own independent copy of the data
    struct Score {
        var points: Int
    }
    
    var playerOne = Score(points: 100)
    var playerTwo = playerOne   // independent copy
    playerTwo.points = 250
    
    print(playerOne.points)  // 100 — not affected
    print(playerTwo.points)  // 250
    
    // Great for: most data models in SwiftUI apps
    // Safe, predictable — no surprising side effects
    Structs are Swift’s preferred model type. Because copies are independent, you never have to worry about one part of your app accidentally changing data that another part is reading. Swift also optimises struct copies efficiently behind the scenes.
    class (reference type) All variables sharing a class instance see the same object
    class GameSession {
        var score: Int = 0
        var level: Int = 1
    }
    
    let sessionA = GameSession()
    let sessionB = sessionA   // same object — NOT a copy
    
    sessionA.score = 500     // changes the shared object
    print(sessionB.score)    // 500 — sessionB sees the change
    
    // Great for: objects shared across many parts of the app
    // Use with care — changes anywhere affect everyone
    Classes are useful when you genuinely want multiple parts of your app to share and mutate the same object — for example, a game session managed by a game engine, or a shared data manager. In SwiftUI, you will see classes used as @Observable or ObservableObject for exactly this reason.
    Challenge 7.4 — Prove the Difference

    Write a struct called ShoppingCart with a single property itemCount (Int). Then write a class called SharedCart with the same property.

    For the struct: create an instance, copy it into a second variable, change the second one’s itemCount, and print both to prove they are independent. For the class: do the same thing and print both to show they share the same data. Add a comment above each print explaining what you expect to see before you run it.

    Hint: You can declare a class the same way as a struct but with the keyword class instead. Remember that classes require you to write your own init — Swift does not provide a memberwise initializer for classes automatically.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain value types and reference types in Swift using two different real-world analogies — one for each. Then tell me why Swift defaults to structs over classes for most data models.
    In what situations would I actually choose a class over a struct in a SwiftUI app? Walk me through some concrete examples — no code, just plain English reasoning.
    Build a Practice Example Get commented code you can study and learn from
    Write the same data model twice in Swift — once as a struct and once as a class. Show a scenario that demonstrates the difference in behaviour when you copy and modify an instance. Add a comment on every line explaining what is happening and why.
    Give me a Swift example where using a struct would cause a bug if I expected reference behaviour. Then show me how switching to a class fixes it. Walk me through what changed and why.
    7.5
    Mutating Methods in Structs
    ⏱ 30 min Swift Structs and Classes

    You have learned that structs are value types. Because of this, Swift treats a struct instance as if it is a snapshot — it expects the struct to stay the same unless you explicitly say otherwise. This creates a rule you will run into quickly: a regular method inside a struct cannot change the struct’s own properties.

    When you do want a method to modify a property, you mark it with the keyword mutating. This tells Swift: yes, this method is allowed to change this struct’s data. Swift will then make sure you only call it on a variable (var), not a constant (let).

    struct StepCounter {
        var steps: Int = 0
        var goal: Int = 10_000
    
        // Regular method — reads data, does not change anything
        func progressMessage() -> String {
            "\(steps) of \(goal) steps"
        }
    
        // mutating — this method is allowed to modify properties
        mutating func addSteps(_ count: Int) {
            steps += count                  // modifies the steps property
        }
    
        // mutating — can change multiple properties
        mutating func reset() {
            steps = 0                       // resets steps back to zero
        }
    
        // computed property — reads goal but does not change anything, so not mutating
        var isGoalReached: Bool {
            steps >= goal
        }
    }
    
    var counter = StepCounter()          // must be var — we plan to mutate it
    counter.addSteps(3_500)
    counter.addSteps(4_200)
    print(counter.progressMessage())     // 7700 of 10000 steps
    print(counter.isGoalReached)          // false
    counter.addSteps(3_000)
    print(counter.isGoalReached)          // true
    counter.reset()
    print(counter.steps)                  // 0
    Output
    7700 of 10000 steps
    false
    true
    0
    LineWhat it does
    func progressMessage()A regular method — it only reads properties, never changes them. No mutating needed.
    mutating func addSteps(_ count: Int)The mutating keyword gives this method permission to modify the struct’s properties.
    steps += countThis is the line that changes the struct. Without mutating, Swift would refuse to compile this.
    var counter = StepCounter()You can only call mutating methods on var instances. If you declared counter with let, the compiler would block the call.
    var isGoalReached: Bool { ... }A computed property that reads data — no mutating needed. Only methods that write to properties require it.
    Why does this rule exist? It comes back to value types. When Swift copies a struct, it wants that copy to be predictable. Requiring you to declare mutating makes it obvious which methods can change data and which ones are safe to call on a copy without side effects. It is a feature, not a limitation.
    mutating with self reassignment A mutating method can replace the entire instance with a new one
    struct TrafficLight {
        var color: String
    
        mutating func next() {
            switch color {
            case "red":    color = "green"
            case "green":  color = "yellow"
            default:       color = "red"
            }
        }
    }
    
    var light = TrafficLight(color: "red")
    light.next()
    print(light.color)  // green
    light.next()
    print(light.color)  // yellow
    A mutating method can change any number of properties, or even replace the entire instance using self = TrafficLight(color: "red"). As long as the method is marked mutating, Swift allows it.
    let vs var with mutating Swift enforces mutating restrictions at the call site
    struct Counter {
        var count: Int = 0
        mutating func increment() { count += 1 }
    }
    
    var mutableCounter = Counter()
    mutableCounter.increment()   // fine — mutableCounter is var
    print(mutableCounter.count)   // 1
    
    let fixedCounter = Counter()
    // fixedCounter.increment()   // error — cannot mutate a let constant
    If you try to call a mutating method on a let constant, Swift will refuse to compile. This is intentional — let means the value will not change, so calling a mutating method would contradict that promise.
    Challenge 7.5 — Habit Tracker

    Create a struct called Habit with properties: name (String), currentStreak (Int), and bestStreak (Int).

    Add a mutating method called completedToday() that increments currentStreak by 1. Inside that method, also check if currentStreak is now greater than bestStreak — if so, update bestStreak to match. Add a mutating method called missedToday() that resets currentStreak to 0. Add a non-mutating method that prints a status message showing both streaks.

    Hint: Both completedToday() and missedToday() modify properties so they both need mutating. The status print method only reads — no mutating needed. Remember to declare your habit instance with var.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Why does Swift require the mutating keyword on struct methods that change properties? Explain the reasoning in plain English — what problem is Swift trying to prevent?
    What happens behind the scenes when I call a mutating method on a struct? What does Swift actually do to the instance? Explain without writing any code first.
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift struct for a simple text editor that tracks word count and character count. Include both mutating and non-mutating methods. Add a comment on every line explaining what it does and why — write for a beginner.
    Give me three examples of Swift structs from real app categories — productivity, health, or finance — that each have at least one mutating method. For each, explain why the mutation makes sense for that type of data.

    Stage 7 Recap

    Here is everything you learned in Stage 7:

    • A struct is a custom data type that groups related properties together — like a blueprint you can use to create instances
    • Properties are the data a struct holds (stored or computed), and methods are the actions it can perform
    • A custom initializer uses init to control how instances are set up, including setting defaults and doing calculations before storing values
    • Structs are value types — copying a struct creates an independent copy. Classes are reference types — copying a class gives you another reference to the same object
    • Methods that modify a struct’s properties must be marked mutating, and can only be called on var instances

    These are the building blocks of every model in a SwiftUI app. Every piece of data you display — a user profile, a to-do item, a product, a message — will be represented using these ideas.

    Up next: SwiftUI. You will take everything you have built here and start creating real visual interfaces — buttons, lists, screens, and navigation — running on a real device.

    Learn Swift Stage 8: Protocols and Extensions

    The most powerful Swift code you’ll ever read isn’t built from fancy tricks — it’s built from protocols and extensions, and Stage 8 is where they finally click.

    All you need to follow along is Xcode and a Swift Playground. No new setup required. Each of the five lessons in this stage takes about 25 to 35 minutes and ends with a hands-on challenge you solve yourself. Don’t skip the challenges — protocols are one of those topics that feel clear when you’re reading but fuzzy until you actually write the code.

    By the end of Stage 8, you’ll understand what a protocol is and how to define one, how to conform your own types to protocols, what the four most common built-in protocols do and when to add them, how extensions let you add behavior to any type including ones you didn’t write, and what some View actually means every time you write a SwiftUI view. These are the concepts that separate code you copy from code you truly own.

    08
    Stage 8
    Protocols and Extensions
    5 lessons · ~2.5 hrs
    8.1
    What a Protocol Is
    ⏱ 25 min Swift Basics

    Think about a job listing. Before anyone gets hired, the company publishes a list of requirements: “You must be able to write reports, attend weekly meetings, and respond to email within 24 hours.” The company doesn’t care how you do those things — it just needs the guarantee that you can. A protocol in Swift works exactly the same way. It’s a list of requirements, not an implementation. It says “anyone who uses this protocol must provide these properties and methods.” It doesn’t say how.

    You’ve already been using protocols without realizing it. Every time you conformed a struct to Identifiable in SwiftUI, or used Codable to decode JSON, you were working with protocols. This lesson pulls back the curtain and shows you what a protocol actually is at the Swift language level, so those familiar names finally make complete sense.

    By the end of this lesson you’ll understand what a protocol is, how to define one yourself, and why they exist. You’ll also understand the key mental model: a protocol is a contract, not an implementation.

    // Define a protocol — a contract that any conforming type must fulfill
    protocol Describable {
        var description: String { get }   // must have a readable description property
        func describe()                      // must have a describe() method
    }
    
    // A struct that fulfills the contract
    struct Book: Describable {
        var title: String
        var description: String { "A book called \(title)" }  // fulfills the property
    
        func describe() {                                         // fulfills the method
            print(description)
        }
    }
    
    let swift = Book(title: "Swift Programming")
    swift.describe()   // A book called Swift Programming
    Output
    A book called Swift Programming
    LineWhat it does
    protocol Describable { }Defines a new protocol named Describable. Everything inside is a requirement — not actual code that runs.
    var description: String { get }Declares that conforming types must have a readable description property of type String. The { get } means it just needs to be readable — it can be a stored or computed property.
    func describe()Declares that conforming types must have a describe() method. No implementation here — just the signature.
    struct Book: DescribableThe colon after Book followed by Describable means “Book promises to fulfill the Describable contract.” This is called conformance.
    var description: String { ... }The struct fulfills the protocol’s property requirement using a computed property. Swift just needs the name, type, and { get } access — this satisfies it.
    func describe() { ... }The actual implementation of the required method. This is where the real code lives — not in the protocol.
    Common confusion: A protocol never contains working code. It contains requirements — signatures, property declarations — but no logic. Think of it as an outline, not a completed essay. All the actual code lives in the types that conform to it.

    What Can a Protocol Require?

    Property requirement Require a stored or computed property
    protocol Named {
        var name: String { get }         // read-only — just needs to be readable
        var nickname: String { get set }  // read-write — must be changeable too
    }
    Use { get } when you just need to read the value. Use { get set } when the protocol also requires the value to be writable. A type can always fulfill a { get } requirement with a stored property — that’s fine.
    Method requirement Require a method with specific parameters and return type
    protocol Greetable {
        func greet() -> String                  // must return a String
        func greetPerson(named: String) -> String  // with a parameter
    }
    A protocol can require methods with any combination of parameters and return types. The conforming type must implement each method with exactly the same signature — the same parameter labels, types, and return type.
    Initializer requirement Require that conforming types have a specific init
    protocol Configurable {
        init(name: String)   // conforming type must have this initializer
    }
    
    struct Widget: Configurable {
        var name: String
        init(name: String) {   // fulfills the init requirement
            self.name = name
        }
    }
    Protocols can also require specific initializers. This is less common for beginners but you’ll encounter it in frameworks. Classes that conform to a protocol with an init requirement must mark that init with the required keyword.
    Multiple protocols A type can conform to more than one protocol
    protocol Flyable {
        func fly()
    }
    
    protocol Swimmable {
        func swim()
    }
    
    // Duck conforms to both — separated by commas
    struct Duck: Flyable, Swimmable {
        func fly()  { print("Flap flap") }
        func swim() { print("Splash splash") }
    }
    A type can conform to any number of protocols. Just list them after the colon, separated by commas. The type must fulfill every requirement from every protocol it lists. This is a key advantage over class inheritance — a struct can’t inherit from two classes, but it can conform to ten protocols.
    SyntaxWhat It Does
    protocol Name { }Defines a new protocol with the given name
    var x: Type { get }Requires a readable property named x
    var x: Type { get set }Requires a readable and writable property named x
    func methodName()Requires a method with the given signature
    struct Foo: ProtocolNameDeclares that Foo conforms to ProtocolName
    struct Foo: A, B, CFoo conforms to protocols A, B, and C
    🏆 Challenge
    The Playable Protocol

    Define a protocol called Playable that requires a title property (read-only String) and a play() method. Then create two structs — Song and Podcast — that both conform to Playable. Each should implement play() differently. Call play() on an instance of each.

    Hint: Write the protocol first, then the structs. Swift will tell you if you missed a requirement — the compiler error is your guide.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    Explain Swift protocols to me like I’ve never seen them before. Use a real-world analogy that has nothing to do with coding. Then show me the simplest possible protocol definition and walk through it line by line.
    I understand that a protocol defines requirements, but I’m fuzzy on why you’d use one instead of just putting those properties and methods directly in a struct. Can you explain the advantage with a simple, concrete example?
    Build a Practice Example Study AI-generated code with heavy inline comments
    Write a short Swift example that defines a protocol with at least one property requirement and one method requirement. Then show two different structs conforming to it. Add a comment on every line explaining what it does and why — write for a complete beginner.
    8.2
    Conforming to a Protocol
    ⏱ 30 min Swift Basics

    Imagine you want to apply for a position that requires a driver’s license, a first aid certificate, and fluent Spanish. You don’t just say “I can do those things” — you go get each one and provide proof. Protocol conformance in Swift works the same way. You claim conformance by adding the protocol name after the colon, and Swift immediately checks whether you’ve actually fulfilled every single requirement. If you’re missing anything, it won’t compile.

    You’ve already written conformance dozens of times. Every struct you wrote in SwiftUI that had an id property was conforming to Identifiable. Every time you wrote struct Model: Codable, you were declaring conformance. This lesson explains exactly what’s happening under the hood when you do that.

    By the end of this lesson you’ll understand how to fulfill protocol requirements correctly, what the compiler checks for, and how to split conformance into an extension to keep your code tidy.

    protocol Vehicle {
        var numberOfWheels: Int { get }    // read-only property requirement
        var color: String { get set }      // read-write property requirement
        func describe() -> String           // method requirement
    }
    
    struct Bicycle: Vehicle {
        var numberOfWheels: Int = 2        // stored property — fulfills { get }
        var color: String                   // stored property — fulfills { get set }
    
        func describe() -> String {          // fulfills the method requirement
            return "A \(color) bicycle with \(numberOfWheels) wheels"
        }
    }
    
    var myBike = Bicycle(color: "red")
    print(myBike.describe())   // A red bicycle with 2 wheels
    Output
    A red bicycle with 2 wheels
    LineWhat it does
    struct Bicycle: VehicleDeclares that Bicycle conforms to the Vehicle protocol. Swift will now check that every requirement is fulfilled.
    var numberOfWheels: Int = 2A stored property with a default value. A stored property always satisfies a { get } requirement because it can be read.
    var color: StringA stored property with no default. It satisfies { get set } because stored properties are both readable and writable by default.
    func describe() -> String { ... }The full implementation of the required method. The signature must match the protocol exactly — same name, same parameters, same return type.
    Fulfilling { get } with a stored property: If the protocol says { get }, you can use a stored property, a computed property with a getter, or even a let constant. All three satisfy a read-only requirement.

    Conformance Patterns

    Conformance in the declaration Most common — list the protocol right where you define the type
    struct Car: Vehicle {          // declare conformance here
        var numberOfWheels: Int = 4
        var color: String
        func describe() -> String {
            return "A \(color) car"
        }
    }
    All requirements are fulfilled inline in the same block. This is fine for simple types. When a type conforms to multiple protocols, it can get crowded — that’s when extensions become useful.
    Conformance in an extension Separate the conformance logic into its own block
    struct Truck {                   // core type defined here
        var color: String
        var payload: Double
    }
    
    extension Truck: Vehicle {       // conformance added separately
        var numberOfWheels: Int { 18 }  // computed property fulfills { get }
        func describe() -> String {
            return "A \(color) truck carrying \(payload)kg"
        }
    }
    This pattern keeps your core type clean and groups protocol-related code together. You’ll see this all over professional Swift code. The extension declares the conformance and provides the implementation in one place.
    Protocol as a type Use the protocol name as the type of a variable
    let vehicles: [any Vehicle] = [
        Bicycle(color: "blue"),
        Car(color: "red"),
        Truck(color: "black", payload: 5000)
    ]
    
    // Call describe() on each — Swift knows they all have it
    for vehicle in vehicles {
        print(vehicle.describe())
    }
    This is one of the most powerful uses of protocols. Because all three types conform to Vehicle, you can store them in the same array and call protocol methods on all of them. Swift guarantees describe() exists because the protocol contract says so.
    Mutating methods Protocol methods that modify struct properties need mutating
    protocol Resettable {
        mutating func reset()   // protocol marks it mutating
    }
    
    struct Counter: Resettable {
        var count = 0
        mutating func reset() {  // struct also marks it mutating
            count = 0
        }
    }
    If a protocol method needs to modify properties on a struct conformer, both the protocol and the struct must mark it mutating. Classes don’t need mutating since they’re reference types, but they can still conform — they just omit the keyword in their implementation.
    Compiler error to recognize: If you declare conformance but don’t implement a requirement, Swift will say something like “Type ‘X’ does not conform to protocol ‘Y'”. This is actually one of the most helpful errors in Swift — it tells you exactly which requirement is missing.
    SyntaxWhat It Does
    struct Foo: Protocol { }Declares Foo conforms to Protocol inline
    extension Foo: Protocol { }Adds conformance to Foo in an extension
    var x: ProtocolA variable that can hold any conforming type
    [any Protocol]An array that can hold any conforming type
    mutating func in protocolAllows struct conformers to modify their own properties
    🏆 Challenge
    The Shape Protocol

    Define a protocol called Shape with a computed property area: Double { get } and a method describe() -> String. Create two structs — Circle (with a radius) and Rectangle (with width and height) — that both conform to Shape using extensions. Then create an array of type [any Shape], add one of each, and loop through printing each shape’s description.

    Hint: For Circle, area = π × r². Swift has Double.pi for the value of π.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    I understand that a type declares conformance with a colon, like struct Foo: MyProtocol. But what exactly does Swift check when I write that? Walk me through what the compiler verifies, step by step.
    What’s the difference between fulfilling a protocol’s { get } property requirement with a stored property versus a computed property? When would you choose one over the other?
    Build a Practice Example Study AI-generated code with heavy inline comments
    Write a Swift example with a protocol, three different structs that conform to it, and a function that accepts [any YourProtocol] and calls a protocol method on each item. Add a comment on every single line — explain what’s happening and why, for a beginner who is seeing protocol conformance for the first time.
    8.3
    Built-in Protocols: Equatable, Comparable, Hashable, Codable
    ⏱ 35 min Swift Basics

    When you buy furniture from a flat-pack store, they include an instruction sheet. But they don’t include instructions for how to breathe while you assemble it — that’s so obvious it goes without saying. Swift’s standard library protocols work similarly: many of them come with sensible default behavior that Swift synthesizes automatically. You just declare conformance and Swift figures out the details.

    You’ve bumped into these four protocols already, even if you didn’t know it. Used == to compare two values? That’s Equatable. Used a struct as a dictionary key? That required Hashable. Decoded a JSON response? That was Codable. These aren’t obscure advanced topics — they’re the everyday protocols you’ll add to almost every model struct you write.

    By the end of this lesson you’ll know what each of these four protocols does, when to add it, and what Swift handles for you automatically versus what you need to implement yourself.

    // Add Equatable so we can use == to compare two Temperatures
    struct Temperature: Equatable, Comparable, Hashable {
        var celsius: Double
    
        // Comparable requires this one method — Swift synthesizes the rest
        static func <(lhs: Temperature, rhs: Temperature) -> Bool {
            return lhs.celsius < rhs.celsius
        }
    }
    
    let boiling  = Temperature(celsius: 100)
    let freezing = Temperature(celsius: 0)
    let body     = Temperature(celsius: 37)
    
    print(boiling == freezing)           // false — Equatable
    print(freezing < boiling)            // true — Comparable
    print([boiling, freezing, body].sorted().first!.celsius)  // 0.0
    Output
    false
    true
    0.0
    LineWhat it does
    : Equatable, Comparable, HashableDeclares conformance to three protocols at once. Swift synthesizes Equatable and Hashable automatically since all stored properties are themselves Equatable/Hashable.
    static func <(lhs:rhs:) -> BoolComparable requires you implement this one operator. Swift synthesizes >, >=, and <= for free based on it.
    boiling == freezingWorks because Temperature is Equatable. Swift synthesized the == implementation by comparing all stored properties.
    .sorted()Works because Temperature is Comparable. Any collection of Comparable values can be sorted without passing a custom comparator.
    Swift synthesis: For structs where all stored properties are already Equatable or Hashable, just declaring conformance is enough — Swift writes the implementation for you. This only works automatically for structs and enums, not classes.

    The Four Protocols Every Beginner Needs

    Equatable Allows comparison with == and !=
    struct Color: Equatable {    // Swift synthesizes == automatically
        var red: Int
        var green: Int
        var blue: Int
    }
    
    let red  = Color(red: 255, green: 0,   blue: 0)
    let red2 = Color(red: 255, green: 0,   blue: 0)
    let blue = Color(red: 0,   green: 0,   blue: 255)
    
    print(red == red2)   // true — all properties match
    print(red == blue)   // false
    Add Equatable when you need to compare two instances with ==. It's also required if you want to use .contains() on an array of your custom type. Swift synthesizes the implementation as long as all stored properties are themselves Equatable — which includes String, Int, Double, Bool, and most standard types.
    Comparable Allows sorting and comparison with <, >, <=, >=
    struct Player: Comparable {
        var name: String
        var score: Int
    
        // Required: define what "less than" means for your type
        static func <(lhs: Player, rhs: Player) -> Bool {
            return lhs.score < rhs.score  // sort by score
        }
    }
    
    var players = [
        Player(name: "Alice", score: 92),
        Player(name: "Bob",   score: 78),
        Player(name: "Chen",  score: 88)
    ]
    
    print(players.sorted().last!.name)  // Alice — highest score
    Unlike Equatable, Comparable is not automatically synthesized for structs — you must implement static func < yourself, because Swift doesn't know which property to sort by. Implement it once and you get .sorted(), .min(), .max(), and all comparison operators for free.
    Hashable Allows use as dictionary keys and in Sets
    struct Point: Hashable {  // Hashable implies Equatable
        var x: Int
        var y: Int
    }
    
    // Can now be used as a Set element
    var visited: Set<Point> = []
    visited.insert(Point(x: 1, y: 2))
    visited.insert(Point(x: 1, y: 2))  // duplicate, won't be added
    print(visited.count)               // 1
    
    // Can also be a dictionary key
    var labels: [Point: String] = [:]
    labels[Point(x: 0, y: 0)] = "Origin"
    Hashable includes Equatable — you don't need to list both. Swift synthesizes the hash function automatically for structs where all stored properties are Hashable. You'll need this any time you want your custom type in a Set or as a Dictionary key.
    Codable Encode and decode to/from JSON and other formats
    struct User: Codable {      // Codable = Encodable + Decodable
        var name: String
        var age: Int
    }
    
    // Encode to JSON
    let user = User(name: "Alice", age: 28)
    let encoder = JSONEncoder()
    if let data = try? encoder.encode(user) {
        print(String(data: data, encoding: .utf8)!)  // {"name":"Alice","age":28}
    }
    
    // Decode from JSON
    let json = """{"name":"Bob","age":35}""".data(using: .utf8)!
    let decoded = try? JSONDecoder().decode(User.self, from: json)
    print(decoded?.name ?? "nil")   // Bob
    Codable is actually a type alias for Encodable & Decodable. Swift synthesizes everything automatically as long as all stored properties are themselves Codable — which covers String, Int, Double, Bool, arrays and dictionaries of Codable types, and optionals of those. This is how you decode every API response in a real iOS app.
    Codable and custom property names: If your JSON uses snake_case keys (like first_name) but your Swift property is camelCase (firstName), set decoder.keyDecodingStrategy = .convertFromSnakeCase — no custom code needed.
    ProtocolWhat It Enables
    EquatableCompare with == and != · use .contains() on arrays
    ComparableCompare with < > · use .sorted() .min() .max()
    HashableUse as Set element or Dictionary key (includes Equatable)
    CodableEncode/decode to JSON and other formats (= Encodable + Decodable)
    IdentifiableUse in SwiftUI List/ForEach — requires var id property
    🏆 Challenge
    The Full Model Struct

    Create a struct called Movie with properties title: String, year: Int, and rating: Double. Conform it to Equatable, Comparable (sort by rating), Hashable, and Codable. Then: create an array of 3 movies, sort them by rating, check if two are equal, put them in a Set, and encode one to JSON and print the result.

    Hint: You only need to write one implementation manually — the static func < for Comparable. Swift synthesizes the rest.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    Can you explain the difference between Equatable, Comparable, and Hashable in Swift? Specifically, when would I need each one, and what happens if I forget to add one of them — what error would I see?
    I've seen Codable used everywhere but I'm not sure I fully understand it. Can you explain what Encodable and Decodable each do, why Codable combines them, and when I'd want to use just one instead of both?
    Build a Practice Example Study AI-generated code with heavy inline comments
    Write a Swift struct that conforms to Equatable, Comparable, Hashable, and Codable. Show examples of using each protocol's capabilities — comparing, sorting, putting in a Set, encoding to JSON. Add a comment on every line explaining what it does and why, targeting a beginner who has just learned about protocols.
    8.4
    Extensions
    ⏱ 25 min Swift Basics

    Picture a house that's already built. You can't tear it down and rebuild it from scratch every time you want a new room — but you can add an extension. You're not changing the original structure; you're building onto it. Swift extensions work the same way. You can add new methods, computed properties, and even protocol conformance to any existing type — including types you didn't write, like Apple's own String, Int, or Array.

    You've actually already used extensions — in Lesson 8.2 you saw how conformance can be added to a type in an extension block. But extensions can do more than just add protocol conformance. They're a powerful organizational tool and one of the reasons real Swift code stays readable even as it grows large.

    By the end of this lesson you'll know how to write extensions on your own types and on Swift's built-in types, what you can and can't add in an extension, and the most common patterns you'll see in real codebases.

    // Extend Swift's built-in String type with a new computed property
    extension String {
        var wordCount: Int {
            // split by whitespace and count non-empty components
            return self.split(separator: " ").count
        }
    
        func shout() -> String {
            return self.uppercased() + "!!!"
        }
    }
    
    let sentence = "Swift is really fun"
    print(sentence.wordCount)   // 4
    print(sentence.shout())     // SWIFT IS REALLY FUN!!!
    Output
    4
    SWIFT IS REALLY FUN!!!
    LineWhat it does
    extension String { }Opens an extension on String. Everything inside is added to every String in your app. You don't own the String type, but you can still extend it.
    var wordCount: Int { ... }A computed property added via extension. Extensions can add computed properties but not stored properties — stored properties would change the type's memory layout, which isn't allowed.
    selfInside an extension, self refers to the instance the method is being called on — exactly like inside a struct or class method.
    func shout() -> String { ... }A new method added to String. After this extension, every string in your project has a .shout() method.
    Extensions can't add stored properties: You can add computed properties and methods, but not stored properties. The reason: adding a stored property would change how much memory a value takes up, and that's fixed at compile time. If you need stored state, it belongs in the original type definition.

    Extension Patterns

    Extending your own type Add new computed properties and methods to a type you wrote
    struct Rectangle {
        var width: Double
        var height: Double
    }
    
    // Add geometry helpers in a separate extension
    extension Rectangle {
        var area: Double      { width * height }
        var perimeter: Double { 2 * (width + height) }
        var isSquare: Bool   { width == height }
    }
    Splitting your own types across extensions is a great way to organize code by purpose. Your core struct stays lean with just the stored properties, and computed helpers live in clearly labelled extension blocks. In large projects, each extension is often in a separate file.
    Extending Int and Double Add convenience helpers to numeric types
    extension Int {
        var isEven: Bool { self % 2 == 0 }
        var squared: Int { self * self }
        func times(_ action: () -> Void) {
            for _ in 0..self { action() }
        }
    }
    
    print(7.isEven)    // false
    print(5.squared)   // 25
    3.times { print("hello") }  // hello hello hello
    Extending Int, Double, and String with domain-specific helpers is very common in real codebases. Use this power thoughtfully — adding too many extensions on built-in types can make code confusing for other developers to read.
    Protocol conformance in extension Add protocol conformance to an existing type retroactively
    struct Product {
        var name: String
        var price: Double
    }
    
    // Add Codable conformance later, in an extension
    extension Product: Codable { }  // empty — Swift synthesizes everything
    
    // Add a custom description in another extension
    extension Product: CustomStringConvertible {
        var description: String {
            "\(name) — $\(String(format: "%.2f", price))"
        }
    }
    You can even add conformance to types you don't own. For example, you could make CLLocationCoordinate2D (Apple's type) conform to one of your own protocols. This is called retroactive conformance and it's one of the most flexible features of the Swift protocol system.
    Organizing a SwiftUI view Split a large view into logical extensions
    struct ProfileView: View {
        var username: String
    
        var body: some View {
            VStack { avatarSection; statsSection }
        }
    }
    
    // Keep sub-views in extensions for readability
    extension ProfileView {
        var avatarSection: some View {
            Image(systemName: "person.circle")
        }
        var statsSection: some View {
            Text(username)
        }
    }
    This is one of the most practical patterns in SwiftUI. Breaking a large view's body into named computed properties via extension makes each piece independently readable. The body property becomes a clear table of contents, and each sub-view is its own neat block.
    SyntaxWhat It Does
    extension TypeName { }Adds new capabilities to TypeName
    extension TypeName: Protocol { }Adds protocol conformance to TypeName
    var computed: Type { expr }Adds a computed property (stored properties not allowed)
    func newMethod() { }Adds a new method to the type
    selfRefers to the current instance inside an extension method
    🏆 Challenge
    Extend the Standard Library

    Write an extension on Double that adds three computed properties: asCurrency: String (formatted with 2 decimal places and a $ sign), asPercentage: String (multiplied by 100 with a % sign), and rounded2: Double (rounded to 2 decimal places). Test all three with a few sample values.

    Hint: String(format: "%.2f", self) formats a Double to 2 decimal places. rounded() is already on Double — think about how to use it for 2 decimal places.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    Why can't Swift extensions add stored properties? I want to understand the reason behind this limitation, not just that it exists. Can you explain it in terms of how value types work in memory?
    I've heard that extensions are used to organize code in large Swift files. Can you walk me through a concrete example of a struct that starts messy and gets reorganized cleanly using extensions? No code yet — just walk me through the thinking.
    Build a Practice Example Study AI-generated code with heavy inline comments
    Write a Swift example where a custom struct is defined, then extended with three separate extension blocks — one for computed helper properties, one for protocol conformance, and one for a display formatting method. Comment every line for a beginner, including why you'd split things across extensions rather than putting everything in the struct itself.
    8.5
    Protocol Extensions and the some Keyword
    ⏱ 30 min Intermediate Swift

    Imagine you're the manager of a team of specialists. Every specialist has their own contract (a protocol). But some things are the same for everyone on the team — like how to write a status report. Instead of putting "write a status report" in every individual contract, you write it once in a shared team handbook that everyone on the team automatically gets. That's a protocol extension: you write default behavior once, and every type that conforms to the protocol gets it automatically.

    You've already seen some View in every SwiftUI view you've written. You've probably typed it dozens of times without fully knowing what it means. This lesson finally explains it — at just enough depth to stop the confusion, without getting into advanced generic theory you don't need yet.

    By the end of this lesson you'll know how to add default implementations to protocols using protocol extensions, and you'll understand what some means so that var body: some View makes complete sense to you.

    protocol Greetable {
        var name: String { get }
        func greet() -> String   // required — but we'll provide a default
    }
    
    // Protocol extension — adds a default implementation
    extension Greetable {
        func greet() -> String {
            return "Hello, I'm \(name)!"   // default implementation
        }
    }
    
    // Uses the default greet() — no need to implement it
    struct Robot: Greetable {
        var name: String
    }
    
    // Overrides the default with its own implementation
    struct Pirate: Greetable {
        var name: String
        func greet() -> String {
            return "Arrr, the name's \(name)!"
        }
    }
    
    let r = Robot(name: "R2-D2")
    let p = Pirate(name: "Blackbeard")
    print(r.greet())   // Hello, I'm R2-D2!        ← uses default
    print(p.greet())   // Arrr, the name's Blackbeard!  ← overrides
    Output
    Hello, I'm R2-D2!
    Arrr, the name's Blackbeard!
    LineWhat it does
    extension Greetable { }Opens a protocol extension. Everything added here becomes default behavior for all types that conform to Greetable.
    func greet() -> String { ... }A default implementation of the protocol's required method. Types that don't provide their own implementation will use this one automatically.
    struct Robot: GreetableRobot only provides name. It doesn't implement greet() — but that's fine because the protocol extension provides a default.
    func greet() in PiratePirate provides its own greet(), which overrides the default. Conforming types can always replace a default implementation with their own.
    Protocol extensions vs class inheritance: Default implementations in protocol extensions are Swift's answer to the code-sharing problem that class inheritance solves in other languages. Structs can't inherit, but they can get shared default behavior through protocol extensions. This is a core design pattern in Swift and the entire SwiftUI framework is built on it.

    The some Keyword Demystified

    The problem some solves Why you can't just use a protocol as the return type directly
    // This would be a problem — "any View" loses type information
    // func makeView() -> any View { Text("hello") }
    
    // some View says: "I return ONE specific View type, I just won't say which"
    func makeView() -> some View {
        Text("Hello from a function")  // Swift knows the return type is Text
    }
    
    // Every SwiftUI view uses this exact pattern
    struct WelcomeView: View {
        var body: some View {          // "body returns some specific View type"
            Text("Welcome!")              // specifically it's a Text — Swift knows
        }
    }
    some View means "this returns one specific concrete type that conforms to View, and I promise it will always be the same type." Swift uses this guarantee to optimize the code. You don't have to write the full type — Text or VStack<TupleView<...>> — you just say some View and let Swift figure it out.
    some vs any Two different ways to use a protocol as a type
    // any: "could be any conforming type — unknown at compile time"
    let vehicles: [any Vehicle] = [Car(), Bicycle(), Truck()]
    
    // some: "one specific type — Swift knows which at compile time"
    func makeVehicle() -> some Vehicle {
        return Car()   // always a Car — never changes
    }
    Use any when you need a collection of mixed types that all conform to a protocol — the type can vary. Use some when returning a single value of a specific type that you don't want to write out — the type is fixed, just hidden. In SwiftUI, some View is always the right choice for body.
    Adding methods via protocol extension Give default behavior to methods not in the protocol definition
    protocol Summarizable {
        var title: String { get }
        var content: String { get }
    }
    
    // Add a bonus method not listed in the protocol
    extension Summarizable {
        func preview() -> String {
            let snippet = String(content.prefix(80))
            return "\(title): \(snippet)..."
        }
    }
    
    struct Article: Summarizable {
        var title: String
        var content: String
    }
    
    let a = Article(title: "Swift Tips", content: "Extensions are powerful...")
    print(a.preview())   // Swift Tips: Extensions are powerful....
    Protocol extensions can also add entirely new methods — ones that aren't even listed as requirements in the protocol. Every conforming type gets these bonus methods automatically. This is how Swift's standard library gives you methods like .sorted(), .map(), and .filter() on every collection type.
    Constrained protocol extensions Add behavior only to conforming types that meet extra conditions
    // Only add this method to arrays of Equatable elements
    extension Array where Element: Equatable {
        func countOccurrences(of item: Element) -> Int {
            return self.filter { $0 == item }.count
        }
    }
    
    let scores = [95, 78, 95, 88, 95]
    print(scores.countOccurrences(of: 95))  // 3
    The where clause adds a condition to the extension — this behavior only appears on arrays whose Element type is Equatable. This is an intermediate-level pattern, but you'll start recognizing it in Swift documentation and your IDE autocomplete. You don't need to master it now — just know it exists.
    The key thing to remember about some: When you see some View in SwiftUI, it means "there is exactly one concrete View type here, and Swift knows what it is, even if you don't write it out." That's why you can't return two different view types from the same body property without wrapping them — some means one specific type.
    SyntaxWhat It Does
    extension Protocol { func x() { } }Adds a default implementation of x() to all conforming types
    some ProtocolReturn type: one specific concrete type conforming to Protocol
    any ProtocolVariable/param type: any conforming type, unknown until runtime
    var body: some ViewThe standard SwiftUI pattern — body returns a specific View type
    extension X where T: Y { }Adds behavior only when a type parameter also meets condition Y
    🏆 Challenge
    Default Behavior

    Define a protocol called Printable with a required property summary: String { get }. Write a protocol extension that adds a default printSummary() method that prints the summary with a prefix like "Summary: ...". Create two structs that conform to Printable — one that uses the default printSummary() and one that overrides it with custom behavior. Call printSummary() on both.

    Hint: The protocol extension's default method can use self.summary to access the required property — because any conforming type is guaranteed to have it.
    Deepen Your Understanding Use AI as a tutor — no code generation yet
    I've been writing "some View" in SwiftUI for a while but never fully understood what "some" means. Can you explain it in plain English without getting into generic theory? Focus on the practical meaning — what does it promise, and why does Swift need that promise?
    How is a protocol extension different from a regular extension? Walk me through the distinction and explain when I'd use each one.
    Build a Practice Example Study AI-generated code with heavy inline comments
    Write a Swift example with a protocol, a protocol extension that adds a default implementation, three structs that conform — two using the default and one overriding it. Add a comment on every line explaining what it does and why, writing for a beginner who just learned about protocols for the first time.

    Stage 8 Recap: Protocols and Extensions

    Protocols and extensions are two of Swift's most important building blocks. Together they let you write flexible, organized code that scales cleanly as your apps grow. Here's what you covered in this stage:

    • Lesson 8.1 — What a Protocol Is: A protocol is a contract that defines requirements — properties and methods — without providing implementations. Any type that conforms to the protocol must fulfill those requirements.
    • Lesson 8.2 — Conforming to a Protocol: Conformance is declared with a colon, just like with a struct. Swift verifies at compile time that every requirement is fulfilled. You can add conformance inline or in a separate extension block.
    • Lesson 8.3 — Built-in Protocols: Equatable lets you use ==, Comparable enables sorting, Hashable allows use in Sets and as Dictionary keys, and Codable powers JSON encode/decode. Swift synthesizes most of these automatically for structs.
    • Lesson 8.4 — Extensions: Extensions let you add computed properties, methods, and protocol conformance to any existing type — including types you don't own. They're a core tool for keeping code organized as it grows.
    • Lesson 8.5 — Protocol Extensions and some: Protocol extensions add default implementations shared by all conforming types. The some keyword means "one specific type conforming to this protocol" — and it's the reason var body: some View works in every SwiftUI view you write.

    If you skipped any of the challenges, now is a great time to go back and complete them. Protocols and extensions are concepts that really click through practice, not just reading.

    Up next is Stage 9: Closures — the powerful functions-as-values concept that powers .map(), .filter(), .forEach(), and SwiftUI animations. Everything you just learned about protocols will make closures easier to understand, because many protocol requirements are fulfilled using closures under the hood.

    Learn Swift Stage 9: Closures

    You already know how to write functions. Closures are what happens when functions grow up — they can be stored in variables, passed as arguments, and returned from other functions, all without needing a name.

    All you need for this stage is Xcode and a Swift Playground — no project setup required. There are 6 lessons, each ranging from 20 to 35 minutes. Don't skip the challenge at the end of each lesson. Reading through the examples builds familiarity, but actually writing the code yourself is where the concept stops feeling abstract and starts feeling real.

    By the end of Stage 9 you'll be able to write closures using both full and shorthand syntax, explain why trailing closure syntax exists and recognize it in SwiftUI code, understand how closures capture values from their surrounding context, know what @escaping means and why the compiler asks for it, and use map, filter, and reduce to transform collections. Closures are the concept that trips up more beginners than almost anything else in Swift. Finishing this stage is a genuine milestone.

    09
    Stage 9
    Closures
    6 lessons · ~3 hrs
    9.1
    What a Closure Is
    ⏱ 25 min Swift Basics

    Think about a sticky note. You can write instructions on it, hand it to someone else, stick it to the fridge, or save it for later. The instructions don't disappear just because you wrote them down — they travel with the note. A closure works the same way: it's a chunk of code you can write once, store, pass around, and execute whenever you need it.

    In Stage 4 you learned to write functions with names. A closure is essentially the same thing — a block of code with parameters and a return value — except it doesn't need a name. You've actually been using closures without realizing it. Every time you've seen code inside curly braces being passed to something (like a button action in SwiftUI), that was a closure.

    By the end of this lesson you'll understand what a closure actually is at a conceptual level, how it relates to the named functions you already know, and how to store a simple closure in a variable so you can call it later.

    // A regular named function you already know
    func greet(name: String) -> String {
        return "Hello, \(name)!"
    }
    
    // The same logic written as a closure stored in a variable
    let greetClosure: (String) -> String = { name in
        return "Hello, \(name)!"
    }
    
    // Calling them looks almost identical
    print(greet(name: "Mia"))          // Hello, Mia!
    print(greetClosure("Mia"))          // Hello, Mia!
    Output
    Hello, Mia!
    Hello, Mia!
    LineWhat it does
    func greet(name:) -> StringA regular named function — you've written these since Stage 4. It has a name, a parameter, and a return type.
    let greetClosure: (String) -> StringA constant whose type is "a closure that takes a String and returns a String". The type annotation describes the shape of the closure.
    = { name inThe opening of the closure. The curly braces wrap the code, and name in names the incoming parameter. The keyword in separates the parameter list from the body.
    return "Hello, \(name)!"The body of the closure — same as it would be inside a regular function.
    greetClosure("Mia")Calling a closure stored in a variable. Notice there are no argument labels — closures don't use them the way named functions do.
    No argument labels: When you call a named function you use the parameter label: greet(name: "Mia"). When you call a closure you don't: greetClosure("Mia"). Closures don't have external argument labels. This catches beginners off guard the first time.

    Ways to Think About Closures

    No-parameter closure A closure that takes nothing and returns nothing
    let sayHello: () -> Void = {
        print("Hello!")
    }
    
    sayHello()  // Hello!
    The type () -> Void means "takes no parameters and returns nothing". This is the simplest possible closure type. You'll see this pattern a lot for button actions and callbacks.
    Closure stored then called Store in a variable, call it later anywhere
    let doubleIt: (Int) -> Int = { number in
        return number * 2
    }
    
    let result = doubleIt(5)
    print(result)  // 10
    The closure is defined once and stored in doubleIt. You can call it as many times as you want after that, just like a regular function.
    Closure passed as argument The real power: hand a closure to a function
    // A function that accepts a closure as a parameter
    func runTwice(action: () -> Void) {
        action()
        action()
    }
    
    // Passing a closure in — code to run twice
    runTwice(action: {
        print("Swift is fun!")
    })
    // Swift is fun!
    // Swift is fun!
    This is the pattern that unlocks closures' real usefulness. The function runTwice doesn't know or care what code it runs — the caller decides by passing in a closure. This is how SwiftUI's Button works with its action parameter.
    Closure returned from function Functions can produce and hand back closures
    func makeMultiplier(factor: Int) -> (Int) -> Int {
        return { number in
            return number * factor
        }
    }
    
    let triple = makeMultiplier(factor: 3)
    print(triple(7))  // 21
    The return type (Int) -> Int means this function returns a closure. You'll understand the factor capture in Lesson 9.4 — for now just notice that the returned closure remembers the factor value it was created with.
    SyntaxWhat It Does
    { }The curly braces that wrap a closure's body
    { name in ... }A closure with one parameter called name
    () -> VoidType for a closure that takes nothing and returns nothing
    (String) -> StringType for a closure that takes a String and returns a String
    let x: (Int) -> Int = { ... }Storing a closure in a constant
    x(5)Calling a closure stored in variable x — no argument labels
    🏆 Challenge
    Your First Stored Closure

    Create a constant called square that stores a closure. The closure should take an Int, multiply it by itself, and return the result. Call it with a few different numbers and print each result. Then write a function called applyTwice that takes an Int and a closure of type (Int) -> Int, applies the closure to the number twice (using the output of the first call as the input to the second), and returns the final result.

    Hint: For applyTwice, call the closure once and store the result in a variable, then call the closure again with that variable.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain what a Swift closure is to me like I've never heard the term before. Use a real-world analogy that has nothing to do with coding, then connect it back to functions I already know. Don't write any code yet — just help me build the mental model first.
    I think closures are just unnamed functions stored in variables, but I'm not sure if that's the complete picture. Can you quiz me on what I know about closures so far? Ask me one question at a time and tell me where my understanding is incomplete or wrong.
    Build a Practice Example Generate heavily commented code to study, not just run
    Write a short Swift example that defines three different closures: one with no parameters, one with one parameter, and one that gets passed to a function. Add a comment on every single line explaining what that line does and why — write the comments for someone who has just learned what closures are.
    9.2
    Closure Syntax Step by Step
    ⏱ 35 min Swift Basics

    Imagine receiving a set of instructions that starts out very formal and verbose — every step labeled, every term defined — and then over time you and the person you're working with develop a shorthand. You both know what you mean, so you drop the obvious parts. Swift's closure syntax works exactly like that. The full form says everything explicitly. Each shorthand step drops something that Swift can already figure out on its own.

    This is the most important lesson in Stage 9. The reason closures look confusing in real code is that you're usually seeing the final shorthand form, with most of the syntax stripped away. By working through each reduction one step at a time — starting from the full explicit version — the shorthand stops looking like magic and starts looking like a logical compression.

    By the end of this lesson you'll be able to write a closure in its full form and explain exactly why each shorthand version is valid. You'll finally understand what $0 and $1 mean, and you'll be able to read the compact closure syntax you see throughout Swift's standard library.

    How to approach this lesson: Don't rush to the shorthand. Read each step, type it into a Playground, confirm it works, and make sure you understand why Swift allows that reduction before moving on. The shorthand only makes sense if the full form makes sense first.

    We'll use the same example throughout all five steps: a closure that takes two integers and returns their sum. Let's start with the most explicit version possible.

    // Step 0: A named function for comparison — the baseline
    func add(a: Int, b: Int) -> Int {
        return a + b
    }
    
    // Step 1: Full explicit closure — nothing omitted
    let addFull: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
        return a + b
    }
    
    print(addFull(3, 4))  // 7
    Output
    7
    PartWhat it does
    let addFull: (Int, Int) -> IntThe type annotation on the variable. Tells Swift: this constant holds a closure that takes two Ints and returns an Int.
    { (a: Int, b: Int) -> Int inThe opening of the closure. The parameter names and types are declared inside the braces, then in marks where the body begins.
    return a + bThe body of the closure. Same as a function body.
    Two places for types: In Step 1 you'll notice the types appear twice — once in the variable's type annotation (: (Int, Int) -> Int) and again inside the closure itself (a: Int, b: Int). That's redundant. The next step removes that redundancy.

    The Five-Step Reduction

    Step 2: Remove inline types Swift infers parameter types from the variable annotation
    // Swift already knows the types from the annotation on the left
    // So we can drop them from inside the closure
    let addStep2: (Int, Int) -> Int = { (a, b) in
        return a + b
    }
    
    print(addStep2(3, 4))  // 7
    Because the variable annotation already declares (Int, Int) -> Int, Swift can infer the types of a and b. Writing them inside the closure was redundant, so we removed them. The parentheses around (a, b) are optional at this point and often dropped too.
    Step 3: Remove return keyword Single-expression closures implicitly return their result
    // When the closure body is a single expression,
    // Swift returns it automatically — no "return" needed
    let addStep3: (Int, Int) -> Int = { a, b in
        a + b
    }
    
    print(addStep3(3, 4))  // 7
    This is called implicit return. When a closure contains a single expression, Swift assumes you want to return it. This only works for single-line bodies. If your closure has multiple statements, you still need return.
    Step 4: Shorthand argument names Replace parameter names with $0, $1, $2...
    // Swift provides automatic names $0, $1, $2 for each parameter
    // When you use them, you drop the parameter list and "in" entirely
    let addStep4: (Int, Int) -> Int = { $0 + $1 }
    
    print(addStep4(3, 4))  // 7
    $0 means "the first argument", $1 means "the second argument". When you use shorthand names, you can drop the a, b in part entirely. This is the form you'll see most often in Swift standard library calls.
    Step 5: Operator shorthand For simple operations, an operator itself is a closure
    // For very simple closures like adding two Ints,
    // Swift accepts an operator as the entire closure
    let numbers = [3, 1, 4, 1, 5]
    let sorted = numbers.sorted(by: <)  // [1, 1, 3, 4, 5]
    print(sorted)
    The < operator is a function that takes two values and returns a Bool — exactly what sorted(by:) expects. So you can pass it directly. This is the most extreme shorthand and only works in specific contexts, but it's worth recognizing when you see it.
    All 5 steps side by side The same closure at every level of verbosity
    // Step 1 — Full explicit
    { (a: Int, b: Int) -> Int in return a + b }
    
    // Step 2 — Types inferred
    { (a, b) in return a + b }
    
    // Step 3 — Implicit return
    { a, b in a + b }
    
    // Step 4 — Shorthand argument names
    { $0 + $1 }
    
    // Step 5 — Operator shorthand (only works in specific contexts)
    +
    All five versions do exactly the same thing. The full version leaves nothing to inference. Each step removes something Swift can figure out on its own. When you encounter compact closure syntax in real code, you can mentally "expand" it back toward Step 1 to understand it.
    SyntaxWhat It Does
    { (a: Int) -> Int in return a }Full explicit form — nothing omitted
    { (a, b) in return a + b }Types inferred from context
    { a, b in a + b }Implicit return (single expression)
    { $0 + $1 }Shorthand argument names, no parameter list
    $0First argument to a closure
    $1Second argument to a closure
    🏆 Challenge
    Write It Five Ways

    Write a closure that takes two String values and returns the longer one (use .count to compare lengths — if they're equal, return the first one). Write this closure five times, once at each level of verbosity from this lesson: full explicit, types inferred, implicit return, shorthand argument names, and — if you can figure out a way — using an operator or method shorthand. Confirm all five produce the same result.

    Hint: For the shorthand version, your condition becomes $0.count >= $1.count ? $0 : $1. For Step 5, think about whether sorted(by:) with a string comparison method could apply here.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    I'm learning the Swift closure syntax shorthand reductions. Can you walk me through the five steps from full explicit syntax to operator shorthand, but pause after each step and make sure I understand why that reduction is valid before moving on? Don't write the code first — explain the reasoning, then show it.
    I see $0 and $1 in Swift closures. Can you explain exactly what they are, when Swift makes them available, and when using them makes code clearer vs. when named parameters are better? No code yet — just the conceptual explanation.
    Build a Practice Example Generate heavily commented code to study, not just run
    Write a Swift closure that multiplies two integers. Show it at all five levels of verbosity, from full explicit to shorthand. Add a comment on every single line explaining what changed from the previous version and why Swift allows that reduction. Write the comments for a beginner who has never seen closure shorthand before.
    9.3
    Trailing Closure Syntax
    ⏱ 20 min Swift Basics

    Picture a recipe that ends with a long set of finishing instructions. Instead of cramming those instructions into the middle of the recipe card, you'd write "see back of card" and put them separately where they're easier to read. Trailing closure syntax is Swift's version of that. When a closure is the last argument to a function, Swift lets you pull it out of the parentheses and write it after them — making the code easier to read.

    You've already seen trailing closure syntax in action. Every time you've written a SwiftUI Button with its action in a separate set of braces, or used ForEach with content in braces — that's trailing closure syntax. It wasn't a special SwiftUI feature. It's a core Swift language rule that SwiftUI takes full advantage of.

    By the end of this lesson you'll understand exactly why trailing closure syntax exists, how to write it, what happens when a function has multiple trailing closure parameters, and why so much SwiftUI code looks the way it does.

    // A function that takes a closure as its last argument
    func repeat(times: Int, action: () -> Void) {
        for _ in 0..times {
            action()
        }
    }
    
    // Standard syntax — closure is inside the parentheses
    repeat(times: 3, action: {
        print("Standard")
    })
    
    // Trailing closure syntax — closure moves outside the parentheses
    repeat(times: 3) {
        print("Trailing")
    }
    Output (each version prints 3 times)
    Standard
    Standard
    Standard
    PartWhat it does
    action: () -> VoidThe last parameter is a closure. This is the condition that makes trailing syntax available.
    repeat(times: 3, action: { ... })Standard syntax — the closure sits inside the parentheses as a labeled argument.
    repeat(times: 3) { ... }Trailing syntax — the closure moves after the closing parenthesis. The label action: is dropped. Both versions are identical to Swift.
    The SwiftUI connection: Button("Tap me") { handleTap() } is trailing closure syntax. The button's action closure is the last parameter, so it moves outside the parentheses. VStack { ... } is the same thing — the content is a closure that's the last (and only) parameter.

    Trailing Closure Patterns

    Only parameter is a closure Drop the parentheses entirely
    func doSomething(action: () -> Void) {
        action()
    }
    
    // When the closure is the only argument, omit () entirely
    doSomething {
        print("Just a trailing closure")
    }
    When a function's only parameter is a closure, you can omit the parentheses completely. You'll see this pattern constantly in SwiftUI — VStack { }, Group { }, and many others work exactly this way.
    Multiple trailing closures Swift 5.3+ syntax for functions with multiple closure parameters
    func loadData(onSuccess: () -> Void, onFailure: () -> Void) {
        // simulated success
        onSuccess()
    }
    
    // Multiple trailing closures — first one has no label,
    // subsequent ones use their parameter labels
    loadData {
        print("Data loaded!")
    } onFailure: {
        print("Something went wrong.")
    }
    When a function has multiple closure parameters, Swift 5.3 added multiple trailing closure syntax. The first closure has no label, subsequent closures use their parameter names as labels. You'll see this in SwiftUI's Alert and animation APIs.
    Trailing closure with return value The closure still returns a value even in trailing position
    func transform(value: Int, using: (Int) -> Int) -> Int {
        return using(value)
    }
    
    let result = transform(value: 10) { $0 * 3 }
    print(result)  // 30
    Trailing closure syntax works regardless of what the closure returns. Here the trailing closure takes the input value and triples it, and the outer function returns that result. This is the kind of compact code you'll see in real Swift.
    SyntaxWhat It Does
    func f(action: () -> Void)Declares a function whose last parameter is a closure
    f(action: { ... })Standard call — closure inside parentheses with label
    f { ... }Trailing syntax — closure outside parentheses, no label
    f(x: 5) { ... }Trailing syntax when there are other non-closure parameters
    f { } label: { }Multiple trailing closures — first unlabeled, rest labeled
    🏆 Challenge
    Rewrite Using Trailing Syntax

    Write a function called buildGreeting that takes a name as a String and a closure of type (String) -> String. The function should call the closure with the name and print the result. Call it twice: once using standard syntax with the closure inside parentheses, and once using trailing closure syntax. Then write a second function called attempt that takes two closures — an onSuccess and an onFailure, both of type () -> Void — and calls the first one. Call it using multiple trailing closure syntax.

    Hint: For the multiple trailing closure call, the first closure has no label and goes directly after the function name in braces. The second closure uses onFailure: { } right after the first closing brace.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain trailing closure syntax in Swift to me. Why does it exist? What problem does it solve? I want to understand the "why" before the "how" — don't show me code yet, just explain the reasoning and then connect it to why SwiftUI looks the way it does.
    I'm looking at SwiftUI code and seeing things like VStack { } and Button("Label") { action() }. Can you explain which parts of those are trailing closures and why, without writing new code? Just explain what I'm already looking at.
    Build a Practice Example Generate heavily commented code to study, not just run
    Write a Swift example that shows the same function being called three ways: standard syntax with the closure inside parentheses, trailing closure syntax, and — if only the closure is a parameter — with parentheses omitted entirely. Add a comment on every line explaining what's happening and why each version is valid Swift. Write comments for a beginner.
    9.4
    Capturing Values
    ⏱ 25 min Intermediate Swift

    Imagine giving someone a polaroid photo before they leave on a trip. That photo is a snapshot of a moment in time — even after the scene changes, the person has a copy of what it looked like when they left. A closure that captures a value works the same way: it grabs a copy of (or a reference to) a variable from its surrounding context and keeps it, even after the code that created that variable has finished running.

    You actually saw this in Lesson 9.1 when makeMultiplier(factor:) returned a closure that used factor — a variable from the outer function. That's value capture in action. The closure "closes over" its environment, which is where the term "closure" comes from.

    By the end of this lesson you'll understand what a capture list is, why closures capture values, what [weak self] means and when you need it, and how to recognize when capturing might cause problems you need to think about.

    // A counter using value capture
    func makeCounter() -> () -> Int {
        var count = 0                    // local variable in makeCounter
        let increment: () -> Int = {   // closure captures count
            count += 1                   // modifies the captured variable
            return count                 // returns its current value
        }
        return increment                 // returns the closure itself
    }
    
    let counter = makeCounter()      // makeCounter finishes, but count lives on
    print(counter())  // 1
    print(counter())  // 2
    print(counter())  // 3
    
    let counter2 = makeCounter()    // new closure, its own independent count
    print(counter2()) // 1 — starts fresh
    Output
    1
    2
    3
    1
    ConceptWhat it means
    CaptureThe closure "grabs" the count variable from its surrounding scope and keeps a reference to it. Even after makeCounter() returns, count lives on inside the closure.
    Reference captureBy default, closures capture variables by reference. When the closure modifies count, it modifies the same variable — not a copy. That's why it increments persistently.
    Independent instancescounter2 is a new closure with its own capture of a new count variable. Calling counter2() starts at 1, independently.
    Closures are reference types: Because closures capture by reference by default, two closures that both capture the same variable will share it. Changes made through one closure are visible when calling the other. This can be surprising if you expect independent copies.

    Capture Lists

    Capture by value with [x] Force a copy at the time the closure is created
    var score = 100
    
    // Capture by reference — sees future changes to score
    let byRef = { print(score) }
    
    // Capture by value — takes a snapshot of score right now
    let byVal = { [score] in print(score) }
    
    score = 200  // change score after closures are created
    
    byRef()   // 200 — sees the updated value
    byVal()   // 100 — sees the value from when it was created
    The square brackets before in are a capture list. Writing [score] tells Swift to make a copy of score at the moment the closure is created. Later changes to the original variable don't affect the captured copy.
    [weak self] Avoid retain cycles in class-based code
    class DataLoader {
        var results: [String] = []
    
        func load() {
            // [weak self] prevents a retain cycle
            // self becomes Optional inside the closure
            someAsyncOperation { [weak self] in
                self?.results.append("new item")
            }
        }
    }
    When a closure inside a class captures self, it creates a strong reference by default. If the object also holds a reference to the closure, neither can be freed — a retain cycle. [weak self] makes self Optional inside the closure, breaking the cycle. That's why you see self?. rather than self. inside closures with [weak self].
    [unowned self] Like weak, but non-optional — use only when certain
    class ViewController {
        func setup() {
            // unowned: self will definitely exist when closure runs
            someOperation { [unowned self] in
                print(self)  // no optional needed — but crashes if self is gone
            }
        }
    }
    [unowned self] avoids retain cycles like [weak self], but doesn't make self Optional. This means you don't need self?., but it will crash if the object has been deallocated when the closure runs. When in doubt, prefer [weak self] — it's safer.
    When to use [weak self]: In practice, if you're in a class and writing a closure that will run asynchronously (like a network callback or a timer) and uses self, add [weak self]. It's a good habit that prevents a common class of memory bugs.
    SyntaxWhat It Does
    { print(x) }Captures x by reference — sees future changes
    { [x] in print(x) }Captures x by value — snapshot at creation time
    { [weak self] in self?.method() }Weak capture of self — prevents retain cycles, self is Optional
    { [unowned self] in self.method() }Unowned capture — no retain cycle, not Optional, can crash
    [x, y] inCapture list with multiple values
    🏆 Challenge
    Capture by Reference vs. by Value

    Declare a variable var multiplier = 2. Create two closures that each take an Int and return an Int — the first captures multiplier by reference, the second captures it by value using a capture list. Change multiplier to 10 after both closures are created. Call both closures with the same input and print the results. Explain in a comment why the results differ.

    Hint: The capture list syntax goes right after the opening brace: { [multiplier] in ... }. The by-reference closure will use the updated value of 10; the by-value closure will still use 2.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain what "capturing a value" means in a Swift closure. Use a real-world analogy to explain the difference between capturing by reference vs. by value, and explain why you'd choose one over the other. Don't write Swift code yet — just help me build the intuition.
    Can you explain what a retain cycle is in plain English, and why [weak self] in a closure prevents it? I don't need a deep dive on ARC — just enough to understand when and why to use [weak self] in practice.
    Build a Practice Example Generate heavily commented code to study, not just run
    Write a Swift example that demonstrates the difference between capturing a variable by reference vs. by value in a closure. Show both versions side by side, and add a comment on every line explaining what's happening and why the outputs differ. Write comments for a beginner who is seeing capture lists for the first time.
    9.5
    @escaping Closures
    ⏱ 20 min Intermediate Swift

    Picture a valet taking your car keys when you arrive at a restaurant. Most of the time, the valet holds the keys temporarily — they're used while you're at dinner and given back when you leave. That's like a normal closure: it's used and released within the function's lifetime. But sometimes a valet parks your car overnight and uses the keys long after you've gone home. That's an escaping closure: it "escapes" the function it was passed to and is used after that function has already returned.

    This comes up most often with asynchronous code. When you make a network request, you pass a completion handler — a closure that runs when the data arrives, which might be seconds after the function that started the request has already finished. That completion handler needs to be marked @escaping because it escapes the function's scope.

    By the end of this lesson you'll understand what @escaping means, why the Swift compiler requires it, where you'll encounter it in real iOS development, and why it matters for how you use self inside those closures.

    // Non-escaping: closure is called immediately, within the function
    func doNow(action: () -> Void) {
        action()   // called here, inline, function is still alive
    }
    
    // Escaping: closure is stored and called later, after function returns
    var savedActions: [() -> Void] = []
    
    func saveForLater(action: @escaping () -> Void) {
        savedActions.append(action)  // stored — escapes the function
    }
    
    saveForLater { print("Running later!") }
    saveForLater { print("Also later!") }
    
    // Run them all at some point after saveForLater has returned
    savedActions.forEach { $0() }
    // Running later!
    // Also later!
    Output
    Running later!
    Also later!
    PartWhat it does
    @escaping () -> VoidThe @escaping attribute marks this closure as one that will outlive the function. Swift requires this annotation whenever a closure is stored or passed to another async operation.
    savedActions.append(action)This is the reason @escaping is needed. Storing the closure in an external array means it will still be alive after saveForLater returns — it has "escaped".
    Non-escaping (no annotation)When a closure is NOT escaping, Swift can optimize it. It's called within the function's lifetime and then discarded. This is the default and the most common case.
    Why the compiler enforces this: Swift's memory management relies on knowing when closures escape. If a closure escapes, it might extend the lifetime of objects it captures. Requiring @escaping forces you to acknowledge this explicitly and prompts you to think about capture semantics like [weak self].

    @escaping in Real iOS Code

    Network completion handler The most common use of @escaping in practice
    // A function that fetches data and calls a completion handler when done
    func fetchUser(id: Int, completion: @escaping (String) -> Void) {
        // Simulating async work — imagine a real network call here
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            completion("User #\(id)")  // called 1 second later
        }
    }
    
    // Usage — the closure runs after fetchUser has already returned
    fetchUser(id: 42) { userName in
        print("Got user: \(userName)")
    }
    This is the pattern you'll see constantly in networking code. fetchUser returns immediately, but the completion handler runs a second later when the data arrives. The closure must be @escaping because it's being passed to DispatchQueue.main.asyncAfter, which will call it in the future.
    @escaping + [weak self] The pairing you'll see everywhere in class-based iOS code
    class ProfileViewModel {
        var userName: String = ""
    
        func loadProfile() {
            // @escaping + [weak self] is the standard pattern
            fetchUser(id: 1) { [weak self] user in
                self?.userName = user  // self? because it might be nil
            }
        }
    }
    Because the completion handler escapes and might run after the ProfileViewModel has been deallocated, you use [weak self] to avoid a retain cycle and prevent a crash. This @escaping + [weak self] combination is one of the most common patterns in UIKit-based iOS code.
    Non-escaping is the default You only need @escaping when Swift tells you to add it
    // No @escaping needed — closure is called synchronously and discarded
    func applyToAll(_ items: [Int], transform: (Int) -> Int) -> [Int] {
        return items.map(transform)  // used right here, synchronously
    }
    
    // Closures passed to map, filter, sort are non-escaping
    Most closures you write — especially those passed to map, filter, sorted, and similar higher-order functions — are non-escaping. You only need to think about @escaping when the closure is being stored or passed to an asynchronous operation. The compiler will tell you if you forget.
    SyntaxWhat It Does
    action: () -> VoidNon-escaping (default) — used and released within the function
    action: @escaping () -> VoidEscaping — closure may be stored or called after the function returns
    @escaping + stored in propertyCommon case: closure is saved for later use
    @escaping + async operationCommon case: passed to DispatchQueue, Timer, URLSession, etc.
    [weak self] in @escaping closureStandard pattern to avoid retain cycles in class-based code
    🏆 Challenge
    Build a Simple Callback System

    Create a struct called EventBus with a property var listeners: [() -> Void] = []. Add a mutating func register(listener:) method that accepts an @escaping closure and appends it to listeners. Add a func fire() method that calls every listener. Register three different closures that print different messages, then call fire() and confirm all three run. Pay attention to why @escaping is required here.

    Hint: The closure needs @escaping because you're appending it to an array — storing it beyond the lifetime of the register function. If you omit @escaping, the compiler will tell you to add it.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain @escaping in Swift closures to me in plain English. What does it mean for a closure to "escape" a function? Why does the compiler require me to mark it? Use an analogy — don't show me code yet.
    Can you explain the connection between @escaping closures and why I need [weak self] inside them? I understand both concepts individually but I'm not clear on why they're usually seen together. Explain without writing code first.
    Build a Practice Example Generate heavily commented code to study, not just run
    Write a Swift example showing a function with a non-escaping closure and then the same function rewritten with an @escaping closure that stores the closure for later use. Show both, and add a comment on every single line explaining what @escaping means for each scenario. Write comments for a beginner.
    9.6
    map, filter, and reduce
    ⏱ 35 min Swift Basics

    Think of a production line in a factory. map is the machine that stamps every item as it passes through — transforming each one. filter is the quality inspector who waves some items through and sends others back. reduce is the final step that takes the whole batch and combines it into one thing — a total count, a sum, a single string. Three operations, all working on collections, each using a closure to describe what to do.

    You've used arrays since Stage 5. You've written for loops to process them. Now that closures make sense, you can replace most of those loops with map, filter, and reduce — and the code will often be shorter, clearer, and easier to read at a glance.

    By the end of this lesson you'll be able to transform an array using map, select elements from an array using filter, combine an array into a single value using reduce, and chain all three together when needed.

    let scores = [72, 88, 45, 95, 61, 90]
    
    // map — transform every element
    let grades = scores.map { score in
        score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F"
    }
    // ["C", "B", "F", "A", "F", "A"]
    
    // filter — keep only elements that pass the test
    let passing = scores.filter { $0 >= 70 }
    // [72, 88, 95, 90]
    
    // reduce — combine all elements into one value
    let total = scores.reduce(0) { $0 + $1 }
    // 451
    
    print(grades)   // ["C", "B", "F", "A", "F", "A"]
    print(passing)  // [72, 88, 95, 90]
    print(total)    // 451
    Output
    ["C", "B", "F", "A", "F", "A"]
    [72, 88, 95, 90]
    451
    FunctionWhat it does
    .map { }Creates a new array of the same length by transforming each element. The closure takes one element and returns the transformed version. The result can be a different type than the original.
    .filter { }Creates a new array containing only elements for which the closure returns true. The result is the same type as the original, but shorter.
    .reduce(0) { $0 + $1 }Combines all elements into a single value. The first argument is the starting value. The closure takes the running total and the next element, and returns the new running total.
    Loop vs. map/filter/reduce: Both are valid. A for loop is sometimes clearer for complex multi-step logic. map/filter/reduce tends to be clearer for simple transformations because the intent is stated upfront: "transform this", "filter this", "reduce this to a single value".

    Each Function in Depth

    map — transform elements Apply a transformation to every element, producing a new array
    let names = ["alice", "bob", "charlie"]
    
    // Transform each String to uppercase
    let uppercased = names.map { $0.uppercased() }
    // ["ALICE", "BOB", "CHARLIE"]
    
    // Transform to character counts — output type changes to Int
    let lengths = names.map { $0.count }
    // [5, 3, 7]
    map always produces an array with the same number of elements as the input. The output type can be anything — converting an array of Strings to an array of Ints is perfectly valid. Think of it as applying the same operation to every element in one step.
    filter — select elements Keep only elements where the closure returns true
    let words = ["swift", "apple", "ios", "swiftui", "xcode"]
    
    // Keep only words that contain "swift"
    let swiftWords = words.filter { $0.contains("swift") }
    // ["swift", "swiftui"]
    
    // Keep only long words
    let longWords = words.filter { $0.count > 4 }
    // ["apple", "swiftui", "xcode"]
    The closure passed to filter must return a Bool. Elements where the closure returns true are included in the result; elements where it returns false are dropped. The resulting array may be shorter than the original.
    reduce — combine to one value Accumulate all elements into a single result
    let prices = [9.99, 14.99, 4.99, 29.99]
    
    // Sum all prices — start at 0.0, add each price to the running total
    let total = prices.reduce(0.0) { runningTotal, price in
        runningTotal + price
    }
    // 59.96
    
    // Build a sentence from an array of words
    let sentence = ["Swift", "is", "great"].reduce("") { $0 + ($0.isEmpty ? "" : " ") + $1 }
    // "Swift is great"
    reduce takes two arguments: an initial value (the starting point) and a closure that takes the running result and the next element, returning the new running result. It can produce any type — a sum, a string, even another array.
    Chaining map, filter, reduce Combine operations in sequence for readable data pipelines
    let transactions = [50, -20, 200, -5, 75, -100]
    
    // Step 1 — filter: keep only positive transactions
    // Step 2 — map: apply a 10% bonus
    // Step 3 — reduce: sum them all up
    let bonusTotal = transactions
        .filter { $0 > 0 }
        .map { Double($0) * 1.1 }
        .reduce(0, +)
    
    print(bonusTotal)  // 357.5
    Chaining creates a readable pipeline. Each step produces a new collection that the next step processes. The + passed to reduce is Step 5 of the syntax reduction from Lesson 9.2 — the operator itself as a closure.
    compactMap Like map, but automatically removes nil results
    let strings = ["1", "two", "3", "four", "5"]
    
    // Int("two") returns nil — compactMap removes those
    let numbers = strings.compactMap { Int($0) }
    // [1, 3, 5]
    
    print(numbers)
    compactMap is like map but the closure returns an optional. Any element where the closure returns nil is automatically removed. This is extremely useful when converting strings to typed values where some conversions might fail.
    FunctionWhat It Does
    array.map { transform($0) }Returns a new array of the same count with each element transformed
    array.filter { $0 > 0 }Returns a new array containing only elements where closure is true
    array.reduce(0) { $0 + $1 }Combines all elements into a single value starting from initial value
    array.reduce(0, +)Shorthand reduce using operator as closure
    array.compactMap { Int($0) }Like map, but removes nil results from the output
    array.filter{}.map{}.reduce()Chain operations into a readable data pipeline
    🏆 Challenge
    Process a Student Report

    Start with this array: let scores = [84, 37, 92, 61, 78, 55, 99, 44, 72, 88]. Use filter to keep only passing scores (60 and above). Use map to convert each passing score to a letter grade: 90+ is "A", 80+ is "B", 70+ is "C", 60+ is "D". Use reduce to count how many "A" grades there are. Finally, chain all three operations together in a single expression. Print the grade array and the A count.

    Hint: For the A count, reduce(0) { $1 == "A" ? $0 + 1 : $0 } is one approach. Alternatively, after getting the grades array, call .filter { $0 == "A" }.count.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain map, filter, and reduce in Swift using a real-world analogy for each one. Help me understand what problem each one solves and when I'd reach for one vs the others. Don't write any code yet — just build the intuition for each.
    I understand map and filter but reduce is still fuzzy. Can you walk me through exactly what happens step by step when reduce runs on an array? Explain what the initial value does and what $0 and $1 represent inside a reduce closure — without writing a full example yet.
    Build a Practice Example Generate heavily commented code to study, not just run
    Write a Swift example that chains map, filter, and reduce together on an array of integers. Add a comment on every single line explaining what that step does, what goes in, and what comes out. Then show how the same result could be achieved with a for loop so I can see the contrast. Write all comments for a beginner.

    Stage 9 Recap: Closures

    You just finished one of the most important stages in the entire Learn Swift curriculum. Closures are the concept that separates beginners who are "following along" from developers who genuinely understand the language — and you've now done the hard work of building that understanding one step at a time.

    • Lesson 9.1 — What a Closure Is: Closures are unnamed, storable functions. They can be stored in variables, passed as arguments to other functions, and returned from functions — giving you flexible, reusable blocks of behavior.
    • Lesson 9.2 — Closure Syntax Step by Step: Swift closure syntax can be progressively reduced from its full explicit form all the way down to shorthand argument names like $0 and $1. Each reduction is valid because Swift infers the missing information from context.
    • Lesson 9.3 — Trailing Closure Syntax: When a closure is the last argument to a function, you can move it outside the parentheses. This is how most SwiftUI code is structured — Button, VStack, ForEach, and more all use trailing closures.
    • Lesson 9.4 — Capturing Values: Closures capture variables from their surrounding scope, either by reference (the default) or by value using a capture list. The [weak self] capture pattern prevents retain cycles in class-based code.
    • Lesson 9.5 — @escaping Closures: A closure that will be stored or called after its enclosing function returns must be marked @escaping. This pattern shows up constantly in completion handlers for networking and asynchronous operations.
    • Lesson 9.6 — map, filter, and reduce: These three higher-order functions use closures to transform collections cleanly and expressively. map transforms, filter selects, and reduce combines — and they can be chained together into readable data pipelines.

    If you skipped any challenges, go back and do them. The challenges are where the concepts actually land — reading about closures and writing closures are very different experiences. The discomfort of figuring it out yourself is the learning.

    Stage 10 covers error handling — throws, try, catch, and Result. Closures come up directly in Stage 10: completion handlers that return a Result type are one of the most common patterns in real iOS networking code, and understanding the closure side of that pattern is exactly what you just built here.

    Learn Swift Stage 10: Error Handling

    Every app you've ever loved has code running behind the scenes that assumes something might go wrong — and Stage 10 is where you learn to write code that's ready for it.

    All you need for this stage is Xcode and a Swift Playground. There are 5 lessons with an estimated time of 20 to 35 minutes each, totalling about 2 hours. Each lesson ends with a challenge — don't skip it. That's where the concept actually moves from "I think I get it" to "I can do this myself."

    By the end of Stage 10, you'll be able to mark a function as throws and explain why, call a throwing function using try inside a do/catch block, define your own custom error types using an enum that conforms to the Error protocol, choose confidently between try? and try!, and decide when a function should throw versus return an optional.

    10
    Stage 10
    Error Handling
    5 lessons · ~2 hrs
    10.1
    Why Error Handling Exists
    ⏱ 20 min Swift Basics

    Imagine you ask a friend to look up a contact in a phone book and they tell you "it might not be there." That's an optional. Now imagine you ask them to go to a store, buy milk, and come back — but the store might be closed, they might not have the right milk, or the card might be declined. Returning nil doesn't tell you which thing went wrong. That's the problem error handling solves.

    You already know optionals from Stage 6. Optionals are perfect when the only thing that can go wrong is "there's no value." But real apps do things that can fail in multiple distinct ways: loading a file, parsing JSON, converting user input. When any of those fail, you want to know why — not just that they did.

    By the end of this lesson, you'll understand the difference between the two failure models, when each is appropriate, and why Swift's error handling system is designed to force you to acknowledge failures instead of quietly ignoring them.

    The Problem with Returning nil

    Consider a function that converts a string the user typed into an age:

    // Returns nil if conversion fails — but WHY did it fail?
    func parseAge(from input: String) -> Int? {
        guard let age = Int(input) else {
            return nil  // Could be non-numeric, negative, or over 150 — same nil for all
        }
        guard age >= 0 else { return nil }
        guard age <= 150 else { return nil }
        return age
    }
    
    let result = parseAge(from: "abc")
    // result is nil — but we don't know if it was non-numeric, negative, or too high
    LineWhat it does
    -> Int?The return type is an optional Int — it can be a number or nil.
    guard let age = Int(input)Tries to convert the string to an Int. If it can't, returns nil.
    guard age >= 0Checks the age is not negative. If it is, returns nil again.
    guard age <= 150Checks the age isn't unrealistically high. Returns nil if so.
    The caller gets nilAll three failure cases look identical to the caller. There's no way to know which one triggered.
    The core problem: Returning nil communicates "it failed" but not "here's why." The caller can't make a smart decision — should it show "please enter a number" or "age cannot be negative"? It can't tell. Error handling lets you pass that information along.

    What Happens at Runtime vs Compile Time

    Xcode catches many problems before you even run your app — type mismatches, missing arguments, using a variable before it's defined. These are compile-time errors. But some things can only go wrong while the app is actually running. A file might not exist. The network might be down. A date string might be in the wrong format. Swift's error handling system deals with these runtime failures.

    Compile-time error Caught by Xcode before you run — won't compile
    let name: Int = "hello"  // Error: cannot assign String to Int
    Xcode shows this as a red error immediately. Your app won't even build. No runtime error handling needed.
    Runtime failure Happens while the app is running — compiler can't prevent it
    // Looks fine to the compiler — but what if the file doesn't exist?
    let data = try Data(contentsOf: someURL)
    The compiler can't know at build time whether the file will exist when the user runs the app. This is exactly the kind of failure that needs proper error handling.
    Optional vs Error Two different failure models for two different situations
    // Optional: one failure mode, no explanation needed
    func findUser(id: Int) -> User? { /* returns nil if not found */ }
    
    // Throwing: multiple failure modes, explanation matters
    func loadUserProfile(id: Int) throws -> UserProfile { /* can fail in 3 ways */ }
    Use an optional when "not found" is the only thing that can go wrong. Use a throwing function when the failure has meaningful causes the caller needs to respond to differently.

    Quick Reference: Optionals vs Errors

    SituationUse
    Value might simply not existOptional (Int?, String?)
    Operation can fail in one known wayOptional is fine
    Operation can fail in multiple distinct waysThrowing function
    Caller needs to know why it failedThrowing function
    Failure should not be silently ignoredThrowing function (Swift forces you to handle it)
    The key insight: Swift's error handling system doesn't just let you report failures — it forces the caller to acknowledge them. You can't call a throwing function and pretend it might not fail. That intentional friction is a feature, not a bug.
    🏋️
    Challenge
    Spot the Failure Modes

    You have a function that takes a username string and validates it for an app's sign-up form. It should reject usernames that are empty, shorter than 3 characters, longer than 20 characters, or contain spaces.

    Write it first using an optional return type. Then write it a second time as a throwing function. Which version gives better feedback to the caller? Write a comment explaining which approach you'd use in a real app and why.

    Hint: You don't need to know the throws syntax yet — use a placeholder comment like // would throw a "too short" error here. The goal is just to think through the two models.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain the difference between returning an optional and throwing an error in Swift. I know what optionals are already. Help me understand when I should choose one over the other, using a real-world analogy from app development.
    Can you quiz me on when to use optionals vs throwing functions in Swift? Give me 4 short scenarios one at a time and ask me which approach I'd use. Tell me if I get one wrong and explain why.
    Build a Practice Example Generate a commented example you can study and learn from
    Write a Swift function that validates a password string. It should fail if the password is too short, has no numbers, or has no uppercase letters. First write it returning an optional, then rewrite it as a throwing function. Add comments on every line explaining what's happening and why — write for a beginner who knows optionals but hasn't seen throwing functions yet.
    10.2
    throws, try, and do/catch
    ⏱ 35 min Swift Basics

    Think about ordering at a restaurant. You make a request (a function call), the kitchen either prepares your meal (success) or comes back to tell you the problem (an error is thrown). You can't just ignore the waiter if they come back — you have to respond. That's exactly how throws, try, and do/catch work together.

    This builds directly on your knowledge of functions from Stage 4. A throwing function is just a regular function with one extra ability: it can stop mid-execution and signal to the caller that something went wrong. The caller uses try to say "I know this might fail" and wraps the call in a do/catch block to handle both outcomes.

    By the end of this lesson, you'll be able to write a function that throws an error, call it correctly using try, and catch the failure with a do/catch block — the complete pattern you'll use everywhere in real iOS apps.

    The Basic Pattern

    import Foundation
    
    // Step 1: Define an error type (a simple enum for now)
    enum AgeError: Error {
        case notANumber    // input wasn't numeric
        case tooYoung      // age was negative
        case unrealistic   // age was over 150
    }
    
    // Step 2: Mark the function with "throws" after parameters
    func parseAge(from input: String) throws -> Int {
        guard let age = Int(input) else {
            throw AgeError.notANumber  // throw stops the function and passes the error up
        }
        guard age >= 0 else {
            throw AgeError.tooYoung
        }
        guard age <= 150 else {
            throw AgeError.unrealistic
        }
        return age  // only reached if all checks pass
    }
    
    // Step 3: Call it with "try" inside a do/catch block
    do {
        let age = try parseAge(from: "25")
        print("Valid age: \(age)")  // prints "Valid age: 25"
    } catch AgeError.notANumber {
        print("Please enter a number")
    } catch AgeError.tooYoung {
        print("Age cannot be negative")
    } catch AgeError.unrealistic {
        print("That doesn't seem right")
    } catch {
        print("Something went wrong: \(error)")  // catch-all for anything unexpected
    }
    Keyword / LineWhat it does
    enum AgeError: ErrorDefines possible error cases. The : Error part tells Swift this enum represents error types.
    throws in the signatureMarks the function as one that might throw. This is a promise to the caller: "you'll need to handle my failure."
    throw AgeError.notANumberImmediately stops the function and sends this error to the caller. Nothing after this line runs.
    try parseAge(from:)Calls a throwing function. The try keyword is required — Swift forces you to acknowledge the call can fail.
    do { }Wraps the risky code. If the try line throws, execution jumps to the matching catch.
    catch AgeError.notANumber { }Handles one specific error case. You can have multiple catch clauses for different errors.
    catch { }A catch-all that handles anything not caught by the specific clauses above. Good practice to always include one.
    Common gotcha: If you call a throwing function without try, Xcode shows an error: "Expression is not 'try' and the called function throws." And if you use try without a do/catch block, Xcode asks you to handle the error. Swift doesn't let you silently ignore throwing functions.

    Variations on the do/catch Pattern

    catch all errors One catch clause for everything
    do {
        let age = try parseAge(from: userInput)
        print("Got age: \(age)")
    } catch {
        // "error" is automatically available — it's the thrown value
        print("Failed: \(error)")
    }
    When you don't need to respond differently to each error case, a single catch handles everything. The implicit error constant is always available inside a bare catch block.
    catch with binding Name the error for inspection
    do {
        let age = try parseAge(from: userInput)
        print("Got age: \(age)")
    } catch let e as AgeError {
        // Only catches AgeError types, binds to "e"
        print("Age error: \(e)")
    } catch {
        print("Other error: \(error)")
    }
    You can catch only errors of a specific type using catch let e as SomeError. Useful when you're calling functions that can throw multiple different error types.
    multiple try in one do Several throwing calls in a single block
    do {
        let age = try parseAge(from: ageInput)        // first risky call
        let name = try validateName(nameInput)         // second risky call
        print("Name: \(name), Age: \(age)")
    } catch {
        print("Validation failed: \(error)")
    }
    A single do block can contain multiple try calls. If any one of them throws, execution jumps to catch immediately — the rest of the do block doesn't run.
    rethrowing functions Passing errors through from a closure
    // rethrows means: "I only throw if the closure passed to me throws"
    func processItems(_ items: [String], using transform: (String) throws -> Int) rethrows -> [Int] {
        return try items.map(transform)
    }
    // You've already used rethrows — map, filter, and forEach all use it!
    rethrows is a modifier for higher-order functions. It means the function itself doesn't throw, but if the closure argument throws, the error is passed through. You've been using this already with map and filter from Stage 9.

    Quick Reference: The Three Keywords

    KeywordWhat It Does
    throwsAdded to a function signature — declares the function can throw an error
    throw someErrorInside a function — stops execution and sends the error to the caller
    tryRequired when calling a throwing function — acknowledges it might fail
    do { }Wraps one or more try calls — execution jumps to catch if anything throws
    catch specificError { }Handles one particular error case
    catch { }Catch-all — handles any error not matched by specific catch clauses above
    rethrowsFunction passes through errors from a closure argument rather than throwing its own
    🏋️
    Challenge
    Score Parser

    A game app receives a score as a string from a server response. Write a function called parseScore(from:) that converts it to an Int and throws errors for three cases: the string isn't a number, the score is negative, or the score is higher than 10,000 (the maximum allowed).

    Then call it three times — once with a valid score, once with "abc", and once with "50000" — and print a different message for each failure case.

    Hint: Define a ScoreError enum first. Then write the function with throws in the signature. Call it inside a do/catch block with at least three specific catch clauses.
    Deepen Your Understanding Clarify the pattern without generating code for you
    Explain how throws, try, and do/catch work together in Swift as if you're describing a conversation between two people. I want to understand the flow: what happens when a throw is triggered, where execution goes, and how the catch blocks decide which one runs.
    What's the difference between "throw" and "throws" in Swift? They look similar but mean different things. Explain both in plain English and tell me where each one appears in code.
    Build a Practice Example Study a commented example that shows the full pattern
    Write a Swift example of a throws/try/do/catch pattern for a function that validates a coupon code string. The function should fail if the code is empty, if it's not exactly 8 characters, or if it doesn't start with "CWC". Add a comment on every line explaining what it does and why — write the comments for a Swift beginner who has never seen throwing functions before.
    10.3
    Custom Error Types with Enums
    ⏱ 25 min Swift Basics

    When a smoke alarm goes off, you want to know more than "something's wrong." Is it smoke, carbon monoxide, a low battery, or a false alarm? Each one requires a different response. In Swift, generic errors are like an alarm with no display — you know it failed but not why. Custom error types give every failure a name and, when needed, extra context.

    This connects directly to the enum knowledge you built in Stage 7. A custom error type is just a Swift enum that conforms to the Error protocol. You already know how to define enums with cases and associated values — adding : Error is the only new piece.

    By the end of this lesson, you'll be able to define your own error enums with meaningful case names, add associated values to carry extra context alongside an error, and conform to LocalizedError so your errors can display readable messages to users.

    Basic Custom Error Enum

    // A custom error type is an enum that conforms to Error
    enum NetworkError: Error {
        case noConnection          // device is offline
        case timeout               // server took too long
        case serverError           // server returned a 5xx status
        case invalidResponse       // couldn't parse what the server sent back
    }
    
    func fetchData(from url: String) throws -> String {
        // In a real app this would make a network request
        let isOnline = false
        guard isOnline else {
            throw NetworkError.noConnection  // throw the named case
        }
        return "{ \"data\": true }"
    }
    
    do {
        let result = try fetchData(from: "https://api.example.com/user")
        print(result)
    } catch NetworkError.noConnection {
        print("Please check your internet connection.")
    } catch NetworkError.timeout {
        print("The request timed out. Try again.")
    } catch {
        print("Unexpected error: \(error)")
    }
    PartWhat it does
    enum NetworkError: ErrorDeclares an error type. The : Error conformance is what makes Swift treat these cases as throwable errors.
    Each caseRepresents one specific failure mode. Names should describe what went wrong, not what the code did.
    throw NetworkError.noConnectionThrows a specific case. The caller knows exactly which kind of failure occurred.
    catch NetworkError.noConnectionMatches against the specific case. Pattern matching works the same way it does in a switch statement.

    Adding Associated Values

    associated values Carry extra context along with the error
    // Associated values let you attach info to an error case
    enum NetworkError: Error {
        case noConnection
        case serverError(statusCode: Int)   // carries the HTTP status code
        case invalidResponse(reason: String) // carries a description
    }
    
    func makeRequest() throws {
        throw NetworkError.serverError(statusCode: 503)  // attach the code when throwing
    }
    
    do {
        try makeRequest()
    } catch NetworkError.serverError(let code) {
        print("Server returned \(code)")  // unwrap it in the catch clause
    } catch {
        print("Error: \(error)")
    }
    Associated values work exactly the same in error enums as in regular enums from Stage 7. You attach them when you throw, and unwrap them in the catch clause using pattern matching.
    LocalizedError Make errors show user-readable messages
    // Conforming to LocalizedError gives your errors a .localizedDescription
    enum NetworkError: Error, LocalizedError {
        case noConnection
        case serverError(statusCode: Int)
    
        // errorDescription is the property LocalizedError requires you to implement
        var errorDescription: String? {
            switch self {
            case .noConnection:
                return "No internet connection. Please check your network."
            case .serverError(let code):
                return "Server error (code \(code)). Please try again later."
            }
        }
    }
    
    // Now you can use .localizedDescription to get the user-friendly message
    let error = NetworkError.noConnection
    print(error.localizedDescription)
    // "No internet connection. Please check your network."
    LocalizedError is a protocol (you know protocols from Stage 8) that adds a errorDescription property to your error type. This lets you display human-readable messages in alerts or UI labels — standard practice in real iOS apps.
    multiple error types Separate enums for separate domains
    // Keep error types scoped to their domain — don't put everything in one enum
    enum ValidationError: Error {
        case emptyField(name: String)
        case invalidEmail
        case passwordTooShort(minimum: Int)
    }
    
    enum DatabaseError: Error {
        case notFound(id: Int)
        case duplicateEntry
        case corruptData
    }
    In a real app, you'll typically have separate error enums for different concerns: one for networking, one for validation, one for data persistence. This keeps each error type focused and easy to read.
    Don't confuse Error conformance with anything else: Adding : Error to your enum doesn't change how enums work — it just registers it with Swift's error system so it can be thrown and caught. All your existing enum knowledge applies exactly as before.

    Quick Reference: Custom Error Patterns

    PatternWhat It Does
    enum E: Error { case x }Basic custom error type with simple cases
    case serverError(code: Int)Error case with associated value carrying context
    enum E: Error, LocalizedErrorAdds user-readable errorDescription property
    var errorDescription: String?Required by LocalizedError — return a human-readable message
    catch MyError.case(let value)Catch and unwrap an error case with an associated value
    🏋️
    Challenge
    Login Validator

    Build a LoginError enum with at least four cases: emptyEmail, invalidEmail, emptyPassword, and passwordTooShort (with an associated value carrying the minimum required length).

    Conform it to LocalizedError and provide a clear user-facing message for each case. Then write a validateLogin(email:password:) function that throws the appropriate error, and call it with a few test inputs to verify your error messages appear correctly.

    Hint: For the invalidEmail check, a simple rule is fine — check that the string contains "@". You don't need real email validation logic.
    Deepen Your Understanding Clarify concepts without writing the code for you
    I know how to use Swift enums with raw values and associated values. How is an error enum different from a regular enum? What does conforming to Error actually add? Explain without writing any code first — I want to understand the concept before I see more examples.
    What's the difference between Error and LocalizedError in Swift? When should I use one vs the other? I'm building iOS apps so I want to know the practical answer, not just the theory.
    Build a Practice Example Study a commented example with associated values and LocalizedError
    Write a Swift custom error enum for a recipe app that loads recipe data from a file. Include cases for file not found, corrupt data, and missing required fields (with an associated value for the field name). Conform it to LocalizedError with user-friendly messages. Add comments on every line explaining the decisions — write for a beginner who just learned about enums and is seeing error enums for the first time.
    10.4
    try? and try!
    ⏱ 20 min Swift Basics

    Sometimes you call a throwing function and you genuinely don't care about the specific error — you just need to know if it worked. Other times, you're absolutely certain it cannot fail, and a full do/catch feels like unnecessary ceremony. Swift has shorthand for both situations: try? and try!.

    These will feel familiar because they mirror the optional concepts you already know from Stage 6. try? is like using optional chaining — it gracefully turns a failure into nil. try! is like force unwrapping — it's a promise to the compiler that you know what you're doing, and it crashes if you're wrong.

    By the end of this lesson, you'll know when try? is genuinely the right tool, understand exactly why try! is dangerous, and be able to make the right call in real code.

    try? — Turning Errors into Optional nil

    // Full do/catch when you need to handle the specific error
    do {
        let age = try parseAge(from: "25")
        print("Age: \(age)")
    } catch {
        print("Error: \(error)")
    }
    
    // try? when you just need nil on failure — same result, less code
    let age = try? parseAge(from: "25")  // age is Int? (25 if success, nil if throws)
    
    // You can use it with optional binding — the familiar if let pattern
    if let validAge = try? parseAge(from: userInput) {
        print("Valid age: \(validAge)")
    } else {
        print("Invalid input")  // we don't know why — and in this context we don't care
    }
    ExpressionWhat it does
    try? parseAge(...)Returns an optional: the success value if no error was thrown, or nil if any error was thrown. The error itself is discarded.
    let age = try? ...age is now Int? instead of Int — it wraps the result in an optional automatically.
    if let validAge = try? ...Combines try? with optional binding — the pattern you already know works perfectly here.
    When try? is the right choice: Use it when the error detail doesn't matter to the caller. A classic example: loading optional cached data. If the cache is missing or corrupt, you just fall back to fetching fresh data — you don't need to know which thing went wrong.

    try! — Dangerous but Sometimes Justified

    try! Crashes immediately if the call throws — use with extreme care
    // try! says "I know this won't fail — crash if I'm wrong"
    let url = URL(string: "https://api.example.com")!  // force unwrap — same danger
    let data = try! Data(contentsOf: Bundle.main.url(forResource: "seed", withExtension: "json")!)
    
    // This is only acceptable because:
    // 1. seed.json is bundled in the app — it WILL exist
    // 2. We control its content — it WON'T be corrupt
    // 3. A crash on startup is better than silent wrong behavior
    
    // What happens if it does throw:
    // CRASH — "Fatal error: expression unexpectedly raised an error"
    try! is the ! force-unwrap operator applied to a throwing call. Just like force unwrapping, it's a promise: "this will not fail." If it does fail, the app crashes immediately. Use only for resources you bundle with your app, where failure means something is catastrophically wrong with the build itself — not user data or network responses.
    try? with nil coalescing Provide a fallback value when try? returns nil
    // ?? works with try? the same way it works with any optional
    let age = (try? parseAge(from: userInput)) ?? 0
    // age is 0 if parsing fails — no do/catch needed
    
    // Common in SwiftUI for loading data with a fallback
    let config = (try? loadConfig()) ?? Config.defaults
    The nil coalescing operator ?? pairs naturally with try?. Wrap the whole try? expression in parentheses first, then apply ??. This gives you a clean one-liner with a default fallback.
    try? in a map or compactMap Transform a collection while silently skipping failures
    let inputs = ["25", "abc", "30", "-5", "40"]
    
    // compactMap + try? filters out nil results — invalid inputs disappear silently
    let validAges = inputs.compactMap { try? parseAge(from: $0) }
    // validAges is [25, 30, 40] — "abc" and "-5" were thrown and become nil, then filtered
    This pairs your closure knowledge from Stage 9 with error handling. compactMap filters out nil values, so combining it with try? gives you a clean way to transform arrays while silently discarding invalid items.
    try! warning: The one place you'll see try! in production code is loading bundled JSON seed files or other resources that ship with the app. Do not use it on network calls, user-provided data, file paths from user input, or anything that could vary at runtime. If you're not sure, use try? or a full do/catch.

    Quick Reference: The Three try Forms

    FormReturnsOn ErrorUse When
    tryThe valueJumps to catchYou need to handle specific error cases
    try?Optional (value or nil)Returns nil silentlyYou only care about success/failure, not the reason
    try!Unwrapped valueCrashes the appBundled resources that will always exist
    🏋️
    Challenge
    Filter Valid Temperatures

    You're building a weather app that receives an array of temperature strings from an API: ["22", "broken", "19", "-300", "28", "abc", "31"]. The valid range is -90°C to 60°C.

    Write a parseTemperature(from:) function that throws for non-numeric input and out-of-range values. Then use compactMap with try? to filter the array down to only valid temperatures. Print the result.

    Hint: The function signature looks like func parseTemperature(from input: String) throws -> Double. Use Double(input) to convert — it returns an optional, so you'll need guard let first.
    Deepen Your Understanding Connect try? and try! to what you already know about optionals
    I know Swift optionals well, including force unwrapping with ! and nil coalescing with ??. How do try? and try! mirror those concepts? Explain the analogy clearly — I want to understand why they feel similar, not just that they exist.
    Give me 5 real-world iOS scenarios and for each one, tell me whether I should use try (with do/catch), try?, or try!. Explain your reasoning for each scenario before I answer — I want to develop intuition for which one to reach for.
    Build a Practice Example See try? used in a realistic SwiftUI context
    Write a small Swift example that uses try? to load a cached value from a throwing function, and falls back to a default using ??. Then show the same logic rewritten with try and do/catch. Add comments on every line comparing the two approaches so I can see the tradeoffs. Write the comments for a Swift beginner who understands optionals.
    10.5
    When to Throw vs Return an Optional
    ⏱ 20 min Swift Basics

    Imagine asking someone for directions. If you're one block away, they just point. But if the destination is complicated, you'd want them to explain the route and flag any issues — "the bridge is closed on Tuesdays." Optionals are the point. Throwing functions are the explanation with warnings.

    You've now seen both tools clearly. The harder question isn't how to use them — it's when. This lesson gives you a practical mental model for making that decision, so when you're designing a function, you reach for the right one the first time.

    By the end of this lesson, you'll have a clear framework for choosing between optionals and throwing functions, plus a sense of how Apple's own APIs make this choice — so you can read the Swift standard library and understand the design decisions behind it.

    The Decision Framework

    // OPTIONAL: absence is the only possible outcome, no explanation needed
    func findUser(id: Int) -> User? {
        return users.first { $0.id == id }  // nil means "not found" — clear and sufficient
    }
    
    // OPTIONAL: string-to-type conversion — absence is the only meaningful result
    let number = Int("abc")  // nil — this is how Apple designed it
    
    // THROWING: multiple failure modes, each meaningful
    func loadFile(at path: String) throws -> Data {
        // Could fail because: file missing, no read permission, corrupted
        return try Data(contentsOf: URL(fileURLWithPath: path))
    }
    
    // THROWING: failure should never be silently ignored
    func savePurchase(_ purchase: Purchase) throws {
        // If this fails, the user needs to know. Returning nil would be wrong.
    }
    Question to AskAnswer → Use
    Can the function fail in more than one meaningful way?Yes → throwing function
    Does the caller need to know why it failed?Yes → throwing function
    Would it be acceptable to silently ignore a failure?No → throwing function
    Is "not found" or "doesn't exist" the only failure mode?Yes → optional
    Is this a simple type conversion?Yes → optional (following Swift convention)

    How Apple Makes This Choice

    Optional initializers Apple uses optionals for simple conversions
    // Int("123") returns Optional — failure mode is simple and singular
    let n = Int("123")  // Int? — either it worked or it didn't
    
    // URL(string:) returns Optional — nil if the string is malformed
    let url = URL(string: "not a url")  // URL? — simple yes/no
    
    // Data(contentsOf:) throws — file operations can fail in many distinct ways
    let data = try Data(contentsOf: someURL)  // throws — gives you the reason
    Notice the pattern: Int() and URL() return optionals because there's only one failure outcome and it needs no explanation. Data(contentsOf:) throws because file-system operations fail in many specific ways the caller needs to respond to.
    Combining both A function can throw AND return an optional
    // This function throws on real errors, but returns Optional for "not found"
    func loadUserIfExists(id: Int) throws -> User? {
        let data = try fetchFromDatabase(id: id)  // can throw DatabaseError
        guard let user = decode(data) else {
            return nil  // no user found — nil is appropriate here
        }
        return user
    }
    
    do {
        if let user = try loadUserIfExists(id: 42) {
            print("Found: \(user.name)")
        } else {
            print("No user with that ID")  // nil result — not an error
        }
    } catch {
        print("Database error: \(error)")  // thrown error — real failure
    }
    You can absolutely combine both. A function that throws handles real errors (database crashed, network down), while a returned optional handles expected absence ("no user found" is not an error — it's a normal state). This is a common pattern in real iOS apps.
    Result type A third option: explicit success or failure value
    // Result is a built-in enum with two cases
    func fetchUser(id: Int, completion: (Result<User, NetworkError>) -> Void) {
        // Used with completion handlers (older async pattern)
        // Modern code uses throws with async/await instead
        completion(.success(User(name: "Alex")))
        // or: completion(.failure(.noConnection))
    }
    
    // Handling a Result
    fetchUser(id: 1) { result in
        switch result {
        case .success(let user):  print("Got \(user.name)")
        case .failure(let error): print("Failed: \(error)")
        }
    }
    Result is a built-in Swift type for older async code using completion handlers. You'll encounter it in existing codebases and some third-party libraries. In modern Swift, throwing functions with async/await are preferred — but knowing Result exists helps you read code you didn't write.
    The honest default: If you're unsure, lean toward throwing. An optional return can always be converted to a try? by the caller if they don't need the error. But if you return an optional and the caller later discovers they needed error details, refactoring is harder.

    Quick Reference: Choosing the Right Tool

    SituationUse
    Simple type conversion (String → Int)Optional initializer (Int?)
    Looking up a value that might not existOptional return (User?)
    Operation with multiple failure modesThrowing function
    Failure that should never be silently ignoredThrowing function
    Callback/completion handler (older async pattern)Result<T, Error>
    Modern async operationasync throws (coming in Stage 12)
    Operation throws AND result might be absentthrows -> T? (combine both)
    🏋️
    Challenge
    Design Review

    You're designing functions for a note-taking app. For each of the following, decide whether to use an optional return type, a throwing function, or both — and write the function signature (just the signature, not the implementation). Then write a one-line comment explaining your choice.

    Functions to design: (1) Find a note by its ID in an array. (2) Save a note to disk. (3) Convert a date string like "2025-01-15" into a Date object. (4) Load all notes from a file (file might be missing, corrupted, or in the wrong format). (5) Check if a tag name is already taken in the user's tag list.

    Hint: Think about whether nil is a meaningful result or whether failure needs explanation. For function (4) especially, ask: which failure case would lead to different UI responses?
    Deepen Your Understanding Build intuition for design decisions — no code writing required
    Give me 6 function scenarios in an iOS app. For each one, ask me whether it should return an optional or throw an error — and why. Tell me if I get it wrong and explain the better choice. I want to build a strong mental model for this decision, not just memorize rules.
    How does Apple decide when to use optionals vs throwing functions in the Swift standard library and Apple frameworks? I want to understand the design philosophy so I can apply it to my own code. Give me 3 specific examples from Apple's APIs that illustrate the pattern clearly.
    Audit Your Own Code Let AI review your design choices — but you stay in the driver's seat
    Here are some function signatures I wrote: [paste your code]. For each one, tell me whether I made the right call using Optional vs throws. If I got one wrong, explain what the better choice would be and why — but don't rewrite the functions for me. I want to understand the reasoning so I can fix them myself.

    Stage 10 Recap: Error Handling

    You've now learned one of Swift's most important — and most practical — systems. Error handling is the difference between apps that crash mysteriously and apps that respond gracefully to things going wrong. Here's what you covered:

    • Lesson 10.1 — Why Error Handling Exists: Optionals tell you something failed; throwing functions tell you why. Use throwing when the failure has multiple meaningful causes the caller needs to distinguish.
    • Lesson 10.2 — throws, try, and do/catch: The core pattern. Mark a function with throws, use throw inside it to stop and report failure, call it with try inside a do/catch block, and handle specific cases with separate catch clauses.
    • Lesson 10.3 — Custom Error Types with Enums: A custom error is just an enum that conforms to Error. Add associated values for context. Conform to LocalizedError to provide user-readable messages in your UI.
    • Lesson 10.4 — try? and try!: try? converts a throwing call into an optional — perfect for when you don't need the error detail. try! crashes on failure — only use it for bundled resources you're certain will always exist.
    • Lesson 10.5 — When to Throw vs Return an Optional: Return an optional when there's one natural failure mode and nil is self-explanatory. Use a throwing function when failure has multiple causes, needs explanation, or should never be silently ignored. You can combine both.

    Don't skip the challenges. Writing the patterns yourself — even in a Playground — is the fastest way to move from "I understand this" to "I can actually use this." Each one is designed around the kind of problem you'll hit in a real app.

    Stage 11 takes you deeper into enums — including indirect enums and how to model complex state with them. You'll notice that the custom error enum patterns from Stage 10 are a natural bridge into that material. The tools are the same; the use cases get richer.

    Learn Swift Stage 11: Enums in Depth

    You've been naming things in code for a while now — but Stage 11 is where you learn to define exactly what something can be, and attach real data to each possibility.

    All you need for this stage is Xcode and a Swift Playground. Stage 11 has six lessons, each designed to take around 20 to 35 minutes. Don't rush through the code examples — read the comments, run the snippets, and actually do the challenge at the end of each lesson. The challenge is not optional busywork. It's where the concept clicks.

    By the end of this stage you'll be able to define enums with associated values that carry different data per case, use raw values to connect enum cases to underlying types, add computed properties and methods directly to an enum, model the loading states of a real feature using an enum, and work with Swift's built-in Result type for handling success and failure. This is the stage where Swift enums start to feel genuinely different from any other language you may have encountered — in the best possible way.

    11
    Stage 11
    Enums in Depth
    6 lessons · ~2.5 hrs
    11.1
    Basic Enums Revisited
    ⏱ 20 min Swift Basics

    Think about a traffic light. It isn't just any color — it can only ever be red, yellow, or green. There's no "sort of green" or "bluish red." A finite, fixed set of possibilities. That's exactly the kind of thing an enum is made for.

    You got a quick introduction to enums back in Stage 7 when we were looking at structs. But at that point, enums were just a footnote. Now they get the full treatment — because once you understand what enums can actually do in Swift, you'll reach for them constantly.

    By the end of this lesson you'll understand what an enum is, why you'd use one instead of a plain string or integer, how switch works with enums, and how to define your own enum from scratch.

    enum TrafficLight {
        case red
        case yellow
        case green
    }
    
    let currentLight: TrafficLight = .red
    
    switch currentLight {
    case .red:
        print("Stop")
    case .yellow:
        print("Slow down")
    case .green:
        print("Go")
    }
    Output
    Stop
    LineWhat it does
    enum TrafficLight { }Declares a new enum type called TrafficLight. The name follows the same capitalized convention as structs and classes.
    case redDefines one possible value. An enum can have as many cases as you need — but each one must be explicitly listed.
    let currentLight: TrafficLight = .redCreates a constant of type TrafficLight and assigns it the value .red. The dot syntax is Swift's way of referring to an enum case.
    switch currentLight { }Switch and enums are a natural pair. Swift requires you to handle every case, so you can't accidentally miss one.
    case .red:Each arm of the switch matches one possible enum value. Notice the dot prefix — same syntax as when you assigned the value.
    Common mistake: Using a String instead of an enum. If you write var light = "red", nothing stops you from accidentally assigning "redd" or "RED" later. An enum makes invalid states impossible — the compiler will reject anything that isn't a defined case.

    Ways to Write Enum Cases

    one per line Most readable — preferred for three or more cases
    enum Direction {
        case north
        case south
        case east
        case west
    }
    Each case gets its own line. This is the most common style and the easiest to read at a glance. Use this when you have three or more cases.
    comma-separated Compact — works for small enums with related cases
    enum Direction {
        case north, south, east, west
    }
    You can list multiple cases on one line separated by commas. Both styles produce identical results. Pick the one that reads better for your specific enum.
    type inference Shorter dot syntax when the type is already known
    var heading: Direction = .north
    heading = .east  // no need to write Direction.east
    Once Swift knows the type, you can drop the enum name and use just the dot. This is why you see .red instead of TrafficLight.red in most code.
    switch exhaustiveness Swift requires every case to be handled
    switch heading {
    case .north: print("Going north")
    case .south: print("Going south")
    case .east:  print("Going east")
    case .west:  print("Going west")
    // No default needed — Swift knows all cases are covered
    }
    Unlike switching on a String or Int, switching on an enum doesn't need a default case if you handle every case explicitly. And if you add a new case to the enum later, Swift will flag every switch that doesn't handle it yet.

    Quick Reference

    SyntaxWhat It Does
    enum Name { }Declares a new enum type
    case caseNameDefines one possible value
    let x: Name = .caseNameCreates a variable/constant of the enum type
    x = .caseNameAssigns a new value using dot syntax (type already known)
    switch x { case .a: }Branches on each possible case — must be exhaustive
    🎯
    Challenge
    Model a Compass

    Create an enum called CompassDirection with four cases: north, south, east, west. Write a function called describe that takes a CompassDirection and uses a switch statement to print a sentence describing what traveling in that direction means. Call it with each case and confirm all four messages print correctly.

    Hint: Your function signature should look like func describe(_ direction: CompassDirection). Remember, you don't need a default case if you handle all four directions explicitly.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain Swift enums to me like I'm a beginner. Why would I use an enum instead of just using a String? Give me a real-world analogy and then walk me through the simplest possible Swift example line by line.
    I think I understand basic Swift enums but I want to make sure. Can you quiz me on them? Ask me one question at a time and tell me where I go wrong.
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift enum for the seasons of the year with a switch statement that prints a description of each one. Add a comment on every line explaining what it does and why — write the comments for a beginner who has never seen an enum before.
    Remember: If AI generates code for you, make sure you can explain every line before moving on. Understanding the output is the goal — not just having working code.
    11.2
    Associated Values
    ⏱ 35 min Intermediate Swift

    Think about a parcel tracking system. A package can be in one of several states: waiting to ship, in transit, delivered, or failed to deliver. But here's the thing — each state carries different information. When it's in transit you want to know the current city. When it's delivered, you want a timestamp. When it failed, you want an error message. Each state has a different shape of data attached to it.

    Until now, an enum case was just a label — it told you what something was, but it couldn't carry any information. Associated values change that completely. This is the feature that makes Swift enums genuinely different from what you've seen in other languages, and it's one of the most useful patterns in all of Swift.

    By the end of this lesson you'll understand how to attach data to individual enum cases, how to extract that data using switch and pattern matching, and why this pattern is so powerful for modeling real-world situations where different states have different data.

    enum ParcelStatus {
        case waitingToShip
        case inTransit(currentCity: String)
        case delivered(at: Date)
        case failed(reason: String)
    }
    
    let status = ParcelStatus.inTransit(currentCity: "Chicago")
    
    switch status {
    case .waitingToShip:
        print("Your parcel is being prepared.")
    case .inTransit(let city):
        print("Your parcel is in \(city).")
    case .delivered(let date):
        print("Delivered on \(date).")
    case .failed(let reason):
        print("Delivery failed: \(reason)")
    }
    Output
    Your parcel is in Chicago.
    LineWhat it does
    case waitingToShipA plain case with no associated value — same as you've seen before.
    case inTransit(currentCity: String)Declares an associated value. The parentheses define what data this case carries. Here: a String labelled currentCity.
    case delivered(at: Date)A different case with different associated data — a Date this time. Each case can have its own type and shape of data.
    ParcelStatus.inTransit(currentCity: "Chicago")Creates a value of the enum. Notice you provide the associated value right here, just like calling a function with a labelled argument.
    case .inTransit(let city):Extracts the associated value during pattern matching. The let binds the value to a local name you can use in that branch.
    Key insight: Associated values are stored with the case — not globally on the enum. A variable of type ParcelStatus holds one case and the data for that case. The other cases' data simply doesn't exist until you switch to them.

    Associated Value Patterns

    multiple values A single case can carry more than one piece of data
    enum Location {
        case coordinates(lat: Double, lon: Double)
        case cityName(String)
    }
    
    let loc = Location.coordinates(lat: 51.5, lon: -0.1)
    
    switch loc {
    case .coordinates(let lat, let lon):
        print("Lat: \(lat), Lon: \(lon)")
    case .cityName(let name):
        print("City: \(name)")
    }
    Multiple associated values are listed in the parentheses separated by commas. When pattern matching, you bind each one individually with its own let.
    if case let Match and extract a single case without a full switch
    if case .inTransit(let city) = status {
        print("Currently in \(city)")
    }
    // Only runs if status is the .inTransit case
    When you only care about one specific case and don't need to handle all of them, if case let lets you pattern match and extract in one line without a full switch block.
    unlabelled values Associated values don't require argument labels
    enum Shape {
        case circle(Double)       // radius
        case rectangle(Double, Double)  // width, height
    }
    
    let s = Shape.circle(5.0)
    
    switch s {
    case .circle(let r):
        print("Area: \(3.14159 * r * r)")
    case .rectangle(let w, let h):
        print("Area: \(w * h)")
    }
    Labels are optional. When omitted, the values are positional. Labels (like currentCity:) make code more readable and are generally preferred, but you'll see unlabelled values in many Swift APIs.
    guard case let Early exit if a specific case isn't matched
    func handleDelivery(_ status: ParcelStatus) {
        guard case .delivered(let date) = status else {
            print("Not delivered yet")
            return
        }
        print("Delivered at \(date)")
    }
    guard case let is the early-exit version. If the status isn't the .delivered case, the function returns immediately. This keeps the happy path un-indented.

    Quick Reference

    SyntaxWhat It Does
    case name(Type)Case with one associated value
    case name(label: Type)Case with a labelled associated value
    case name(Type, Type)Case with multiple associated values
    case .name(let x):Extract associated value in a switch
    if case .name(let x) = valMatch one case without a full switch
    guard case .name(let x) = val else { }Early exit if case doesn't match
    🎯
    Challenge
    Model a Payment Method

    Create an enum called PaymentMethod with three cases: cash with an associated Double for the amount, creditCard with an associated String for the last four digits, and applePay with no associated value. Write a function called describePayment that takes a PaymentMethod and prints a human-readable description of the payment. Test it with all three cases.

    Hint: Your cash case might look like case cash(amount: Double). When you extract it in the switch, use case .cash(let amount): to get the value.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain Swift enum associated values to me like I'm a beginner. What makes them different from regular enum cases? Use a real-world scenario and walk me through it step by step — don't write the code yet, just explain the concept.
    I'm confused about when to use associated values versus just putting data in a separate variable. Can you explain the tradeoffs and give me two or three examples of situations where associated values are clearly the right choice?
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift enum called NetworkResponse with cases for success (carrying a String of data), failure (carrying an Error), and loading with no associated value. Then write a switch statement that handles all three. Add a comment on every line explaining what it does and why — write for a beginner.
    Remember: Before moving on, make sure you can explain what case .inTransit(let city): is doing in plain English. If you can describe it out loud to yourself, you've got it.
    11.3
    Raw Values
    ⏱ 20 min Swift Basics

    Think about the number keys on a keyboard. Each key has a label you see — "1", "2", "3" — but behind the scenes, each key also corresponds to a specific numeric code. The label is what you work with; the underlying value is what the computer stores. Raw values work exactly the same way for enum cases.

    You've seen associated values, which let each case carry different, unique data at runtime. Raw values are different: they're fixed at compile time. Every case gets one predetermined underlying value — a String, an Int, or another simple type — and that mapping never changes.

    By the end of this lesson you'll know how to declare an enum with raw values, how to initialize an enum from a raw value (and why that might fail), and when to reach for raw values versus associated values.

    enum Planet: Int {
        case mercury = 1
        case venus   = 2
        case earth   = 3
        case mars    = 4
    }
    
    // Access the raw value
    let position = Planet.earth.rawValue  // 3
    
    // Initialize from a raw value — returns an Optional
    let maybePlanet = Planet(rawValue: 4)   // Planet.mars
    let notAPlanet  = Planet(rawValue: 99)  // nil
    
    print(position)          // 3
    print(maybePlanet ?? "unknown")
    Output
    3
    mars
    LineWhat it does
    enum Planet: IntThe colon followed by a type declares the raw value type. Every case must have a raw value of this type.
    case mercury = 1Assigns the fixed raw value 1 to the mercury case. This mapping is baked in at compile time.
    .earth.rawValueThe rawValue property returns the underlying value for any case. Here it returns 3.
    Planet(rawValue: 4)Creates an enum value from a raw value. This returns an Optional because the raw value might not match any case.
    Planet(rawValue: 99)Returns nil because 99 doesn't match any defined case. Always handle this Optional safely.
    Raw values vs associated values: Raw values are fixed and defined in the code — every case of the same enum always has the same raw value. Associated values are provided at runtime when you create the enum value and can be different every time. You cannot use both on the same enum case.

    Raw Value Patterns

    String raw values Map cases to String values — great for APIs and JSON keys
    enum HTTPMethod: String {
        case get    = "GET"
        case post   = "POST"
        case delete = "DELETE"
    }
    
    var request = URLRequest(url: someURL)
    request.httpMethod = HTTPMethod.post.rawValue  // "POST"
    String raw values let you use readable enum cases in code while producing the exact strings APIs expect. This is a very common pattern when working with networking code.
    implicit raw values Int and String enums can auto-assign raw values
    // Int: starts at 0, increments automatically
    enum Level: Int {
        case beginner, intermediate, advanced
        // rawValues: 0, 1, 2
    }
    
    // String: raw value matches the case name
    enum Color: String {
        case red, green, blue
        // rawValues: "red", "green", "blue"
    }
    For Int enums, Swift auto-assigns starting from 0 (or from wherever you last set a value). For String enums, the raw value defaults to the case name. You only need to write explicit values when the defaults aren't what you want.
    CaseIterable Get all cases as a collection
    enum Weekday: String, CaseIterable {
        case monday, tuesday, wednesday
        case thursday, friday
    }
    
    for day in Weekday.allCases {
        print(day.rawValue)
    }
    // monday, tuesday, wednesday, thursday, friday
    Adding CaseIterable gives your enum an allCases property — an array of every case in the order they were defined. Extremely useful for building pickers, menus, or iterating over every option.
    safe initialization Handle the optional from rawValue init safely
    let code = 3
    
    if let planet = Planet(rawValue: code) {
        print("Found planet: \(planet)")
    } else {
        print("No planet with that number")
    }
    Raw value initialization always returns an Optional. Use if let or guard let to safely unwrap it. Never force-unwrap with ! unless you are 100% certain the value maps to a valid case.

    Quick Reference

    SyntaxWhat It Does
    enum Name: Int { }Enum with Int raw values
    enum Name: String { }Enum with String raw values
    case name = valueExplicitly assigns a raw value to a case
    .caseName.rawValueReads the raw value of a case
    Name(rawValue: x)Creates an enum from a raw value — returns Optional
    enum Name: String, CaseIterableAdds allCases property for iterating every case
    🎯
    Challenge
    HTTP Status Codes

    Create an enum called HTTPStatus with an Int raw value. Add cases for ok (200), notFound (404), serverError (500), and unauthorized (401). Write a function that takes an Int, tries to initialize an HTTPStatus from it, and prints a description if it succeeds or "Unknown status code" if it doesn't. Also make it conform to CaseIterable and print all cases at the end.

    Hint: Use if let status = HTTPStatus(rawValue: code) inside your function to safely handle the Optional.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain the difference between Swift enum raw values and associated values in plain English. When would I choose one over the other? Give me two concrete examples — one where raw values are clearly the right choice, one where associated values are.
    Why does initializing a Swift enum from a rawValue return an Optional? Walk me through the reasoning without writing any code.
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift enum with String raw values for days of the week, conforming to CaseIterable. Then demonstrate: reading a rawValue, initializing from a rawValue safely, and iterating all cases. Add inline comments on every line explaining what's happening and why — write for a beginner.
    Tip: If you're working with an API that sends numeric codes or string keys, raw value enums are often the cleanest way to translate those values into something safe and readable in your Swift code.
    11.4
    Enums with Methods and Properties
    ⏱ 25 min Intermediate Swift

    Think about a vending machine. It knows what state it's in — idle, dispensing, or out of stock — but it also knows how to do things based on that state: display a message, accept a coin, or refuse a selection. The state and the behavior belong together. That's the same idea when you add methods and properties to an enum.

    This surprises a lot of developers coming from other languages, where enums are glorified integers and nothing more. In Swift, an enum can have computed properties and methods just like a struct. The enum case is always available inside those methods as self.

    By the end of this lesson you'll know how to add computed properties to an enum, how to write methods that switch on self, and why keeping behavior close to the type that owns it makes your code cleaner.

    enum Planet: Int {
        case mercury = 1, venus, earth, mars
    
        // Computed property
        var distanceFromSunDescription: String {
            switch self {
            case .mercury: return "Closest to the sun"
            case .venus:   return "Second from the sun"
            case .earth:   return "Third from the sun"
            case .mars:    return "Fourth from the sun"
            }
        }
    
        // Method
        func hasLiquidWater() -> Bool {
            return self == .earth
        }
    }
    
    let home = Planet.earth
    print(home.distanceFromSunDescription)
    print(home.hasLiquidWater())
    Output
    Third from the sun
    true
    LineWhat it does
    var distanceFromSunDescription: String { }A computed property on the enum. The syntax is identical to a computed property on a struct — it returns a value based on some logic.
    switch selfInside an enum method or property, self refers to the current case. Switching on self is the most common pattern for providing different behavior per case.
    func hasLiquidWater() -> BoolA method on the enum. Works exactly like a struct method — takes parameters, returns values, has access to self.
    self == .earthCompares the current case to .earth. Enums with no associated values are automatically Equatable in Swift.
    Why this matters: Without enum methods, you'd write switch planet { } scattered throughout your codebase. With methods, the logic lives on the type itself — one place to read, one place to change.

    Method and Property Patterns

    computed property Expose information about each case as a property
    enum Season {
        case spring, summer, autumn, winter
    
        var emoji: String {
            switch self {
            case .spring: return "🌸"
            case .summer: return "☀️"
            case .autumn: return "🍂"
            case .winter: return "❄️"
            }
        }
    }
    
    print(Season.summer.emoji)  // ☀️
    Any data that can be derived from knowing which case you are belongs in a computed property. You can have as many computed properties as you need.
    method with parameter Methods can accept parameters just like struct methods
    enum Discount {
        case none
        case percentage(Double)
        case fixed(Double)
    
        func apply(to price: Double) -> Double {
            switch self {
            case .none:               return price
            case .percentage(let pct): return price * (1 - pct)
            case .fixed(let amount):  return price - amount
            }
        }
    }
    
    let sale = Discount.percentage(0.2)  // 20% off
    print(sale.apply(to: 100.0))        // 80.0
    This pattern — enum with associated values plus a method that operates on them — is extremely powerful. The method knows which case it is and can extract the associated value right inside the switch.
    mutating method Methods that change the case must be marked mutating
    enum TrafficLight {
        case red, yellow, green
    
        mutating func next() {
            switch self {
            case .red:    self = .green
            case .green:  self = .yellow
            case .yellow: self = .red
            }
        }
    }
    
    var light = TrafficLight.red
    light.next()
    print(light)  // green
    A method that assigns to self must be marked mutating — same rule as structs. This means you must use var, not let, for the variable you call it on.

    Quick Reference

    SyntaxWhat It Does
    var name: Type { switch self { } }Computed property that branches on the current case
    func name() -> Type { switch self { } }Method that returns different values per case
    mutating func name() { self = .otherCase }Method that changes the case — requires var, not let
    switch self { case .x(let v): }Pattern match and extract associated value inside a method
    🎯
    Challenge
    Vending Machine States

    Create an enum called VendingMachineState with three cases: idle, dispensing(item: String), and outOfStock. Add a computed property called statusMessage that returns a user-facing String for each state. Add a method called canAcceptPayment that returns a Bool — true only when idle. Create a few test values and verify your property and method work correctly.

    Hint: Inside the statusMessage property, when you handle the .dispensing case, use case .dispensing(let item): to extract the item name and include it in the returned string.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain why you might want to add a method or computed property to a Swift enum instead of just writing a function that takes the enum as a parameter. What are the tradeoffs? Don't write code — just explain the design reasoning.
    What's the difference between a computed property and a method on a Swift enum? When would I prefer one over the other?
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift enum for pizza sizes (small, medium, large) with a computed property that returns the diameter in inches and a method that calculates the price given a base price per inch. Add a comment on every line explaining what it does and why — write for a beginner who hasn't seen enum methods before.
    11.5
    Using Enums to Model State
    ⏱ 25 min Intermediate Swift

    Think about a recipe loading screen in an app. The screen can be in one of four states: it's fetching data from the network, it has loaded recipes successfully, it hit an error, or the user hasn't searched for anything yet so there's nothing to show. Each state requires completely different UI. Each state might need completely different data.

    This is where everything you've learned in this stage comes together. You now know enums can have associated values, methods, and properties. And the single most important real-world application of all that in iOS development is modeling the possible states of a feature. This is a pattern you will use in almost every app you build.

    By the end of this lesson you'll have a working mental model for state-driven UI — how an enum drives what the user sees — and you'll understand why this pattern makes apps easier to reason about than using a pile of Bool flags and optional variables.

    // The four possible states of a recipe list feature
    enum RecipeListState {
        case idle                        // nothing searched yet
        case loading                     // network request in flight
        case loaded([String])            // recipes arrived
        case failed(message: String)    // something went wrong
    }
    
    // In a SwiftUI view model, you'd have a @Published property like this:
    var state: RecipeListState = .idle
    
    // And in your SwiftUI view body you'd switch on it:
    func describeCurrentState(_ state: RecipeListState) -> String {
        switch state {
        case .idle:
            return "Search for a recipe above."
        case .loading:
            return "Loading..."
        case .loaded(let recipes):
            return "Found \(recipes.count) recipes."
        case .failed(let message):
            return "Error: \(message)"
        }
    }
    
    print(describeCurrentState(.loaded(["Pasta", "Pizza", "Salad"])))
    Output
    Found 3 recipes.
    LineWhat it does
    case idleRepresents the initial state. No data needed — the user just opened the screen.
    case loadingA network request is in progress. No data yet. The UI should show a spinner.
    case loaded([String])Associated value carries the actual data — here, an array of recipe names. The UI renders the list from this.
    case failed(message: String)Something went wrong. The associated value is the error message to show the user.
    switch state { }The view decides what to show by switching on state. Every possible state is handled — no edge case can slip through.
    The SwiftUI connection: In a real SwiftUI app, this enum would live in your view model as a @Published var state: RecipeListState = .idle. Your SwiftUI view would switch on it to decide which child view to show. Change the state, and SwiftUI automatically re-renders the right UI. This is state-driven development in practice.
    Why not just use Bool flags? You might think: "I'll use isLoading: Bool, hasError: Bool, and recipes: [String]?." The problem is that combinations like isLoading = true and hasError = true at the same time are nonsensical but nothing stops them from happening. An enum makes illegal combinations impossible.

    State Modeling Patterns

    generic LoadingState A reusable pattern for any feature that loads data
    // A generic version you can reuse across your app
    enum LoadingState<T> {
        case idle
        case loading
        case loaded(T)
        case failed(Error)
    }
    
    // Use it with any data type
    var recipesState: LoadingState<[String]> = .idle
    var userState:    LoadingState<User>   = .idle
    Using generics (the <T>) lets you define this pattern once and reuse it everywhere in your app. You'll see this exact pattern in real codebases — often called ViewState or AsyncState.
    computed helper properties Expose convenient Boolean checks on state
    extension RecipeListState {
        var isLoading: Bool {
            if case .loading = self { return true }
            return false
        }
    
        var recipes: [String] {
            if case .loaded(let items) = self { return items }
            return []
        }
    }
    
    // Now you can do clean checks in your UI
    if state.isLoading { /* show spinner */ }
    Adding convenience properties to your state enum in an extension keeps the UI code clean. A SwiftUI view can check state.isLoading instead of writing a full if case .loading = state every time.
    state transitions Functions that move state forward in a controlled way
    func fetchRecipes() async {
        state = .loading
        do {
            let results = try await networkCall()
            state = .loaded(results)
        } catch {
            state = .failed(message: error.localizedDescription)
        }
    }
    This is how state transitions happen in a real app. The function walks the enum through its lifecycle: idle to loading, then either loaded or failed. SwiftUI re-renders automatically at each step because state is @Published.

    Quick Reference

    PatternWhat It Does
    enum State { case idle, loading, loaded(T), failed(Error) }The standard four-case loading state pattern
    @Published var state: MyState = .idleObservable state in a SwiftUI view model
    switch state { case .loaded(let data): }Drive UI from state in a SwiftUI view body
    if case .loading = self { return true }Convenience Boolean check on a specific case
    state = .loading / .loaded(data) / .failed(error)Transition state during a network call
    🎯
    Challenge
    Weather App State

    Create an enum called WeatherState that models a weather app screen. It should have cases for: idle (user hasn't entered a city), loading, loaded with associated values for temperature (Double) and condition (String), and failed with an associated error message. Add a computed property called displayMessage that returns a human-readable String for each state. Write a function that simulates fetching weather by accepting a Bool for success, and transitions state from loading to loaded or failed accordingly.

    Hint: Your loaded case might look like case loaded(temperature: Double, condition: String). The function should set state to .loading, then immediately set it to either .loaded(temperature: 22.5, condition: "Sunny") or .failed(message: "City not found") based on the Bool parameter.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain to me why using an enum to model UI state is better than using separate Bool flags and optional variables. Walk me through the problems that Bool flags create and how an enum solves each one.
    I want to understand how a state enum connects to SwiftUI. Without writing code, can you explain what happens at each step — from defining the enum, to observing it in a view model, to using it in a SwiftUI view body?
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift example of a LoadingState enum with four cases — idle, loading, loaded with an array of strings, and failed with an error message. Then write a function that takes a LoadingState and returns the appropriate UI text for each case. Add a comment on every line explaining the decision and the pattern — write for a beginner connecting Swift enums to real iOS development for the first time.
    11.6
    The Result Type
    ⏱ 20 min Intermediate Swift

    Think about a coin flip: it can only come up heads or tails. There's no third option. When you call a function that might succeed or might fail, the outcome is structurally the same — either you got what you asked for, or something went wrong. Swift's built-in Result type captures that binary outcome in a clean, explicit way.

    Back in Stage 10 you learned about error handling with throws, try, and do/catch. Result is a related tool — and now that you understand associated values, you can see exactly how it works under the hood. Result<Success, Failure> is just an enum with two cases: .success(value) and .failure(error).

    By the end of this lesson you'll know how to return a Result from a function, how to switch on it, how to use its built-in helper methods, and when you'd choose it over throwing functions.

    enum NetworkError: Error {
        case badURL
        case noData
        case decodingFailed
    }
    
    // A function that returns a Result instead of throwing
    func fetchUsername(for id: Int) -> Result<String, NetworkError> {
        guard id > 0 else {
            return .failure(.badURL)
        }
        // Simulating a successful response
        return .success("chris_ching")
    }
    
    // Calling the function and handling the result
    let result = fetchUsername(for: 42)
    
    switch result {
    case .success(let username):
        print("Hello, \(username)!")
    case .failure(let error):
        print("Error: \(error)")
    }
    Output
    Hello, chris_ching!
    LineWhat it does
    Result<String, NetworkError>The return type. The first type parameter is the success value type; the second is the error type. Both are specified at compile time.
    return .failure(.badURL)Returns the failure case with an associated error value. No need to throw — you return normally, just with a failure result.
    return .success("chris_ching")Returns the success case with the actual value wrapped inside it.
    switch result { case .success(let username): }Pattern matching on Result — same as any other enum with associated values. Extract the value or the error and handle each case.
    Under the hood: Result is literally just an enum defined in the Swift standard library. It has two cases with associated values — case success(Success) and case failure(Failure). Now that you understand associated values, you could define it yourself.

    Working with Result

    .get() Convert a Result into a throwing expression
    do {
        let username = try result.get()
        print("Got: \(username)")
    } catch {
        print("Failed: \(error)")
    }
    .get() is a bridge between Result and throwing functions. If the result is a success it returns the value; if it's a failure it throws the error. This lets you use Result with existing do/catch code.
    .map() Transform the success value without unwrapping
    let uppercased = result.map { $0.uppercased() }
    // If result was .success("chris_ching"), uppercased is .success("CHRIS_CHING")
    // If result was .failure(error), uppercased is still .failure(error) — unchanged
    .map() applies a transformation to the success value only, leaving failures untouched. This is the same map concept you used with arrays and optionals — consistent across Swift's types.
    throws vs Result When to choose one over the other
    // Use throws when the caller should handle the error immediately
    func loadFile() throws -> String { /* ... */ }
    
    // Use Result when you need to store or pass along the outcome
    func fetchData(completion: (Result<Data, Error>) -> Void) { /* ... */ }
    
    // In async/await code you'll mostly use throws
    func fetchDataAsync() async throws -> Data { /* ... */ }
    In modern Swift with async/await, you'll mostly use throws. Result is especially useful when working with callback-based APIs (completion handlers), or when you want to store the success/failure outcome in a variable to inspect later.
    converting from throws Wrap a throwing call in a Result
    let fileResult = Result { try loadFile() }
    // fileResult is Result<String, Error>
    // Success if loadFile() returned, failure if it threw
    You can wrap any throwing expression in Result { try ... } to capture the outcome as a Result value. This is a clean bridge when you have a throwing function but need a Result.

    Quick Reference

    SyntaxWhat It Does
    Result<Success, Failure>The Result type — Failure must conform to Error
    return .success(value)Return a successful result with an associated value
    return .failure(error)Return a failure result with an associated error
    switch result { case .success(let v): }Pattern match on the result — same as any enum
    try result.get()Throw on failure, return value on success
    result.map { transform($0) }Transform the success value, pass failures through unchanged
    Result { try throwingCall() }Capture a throwing expression as a Result
    🎯
    Challenge
    Safe Division

    Create an enum called MathError that conforms to Error and has one case: divisionByZero. Write a function called safeDivide that takes two Doubles and returns a Result<Double, MathError>. If the divisor is zero, return a failure with .divisionByZero. Otherwise return the quotient as a success. Test it with both a valid division and a division by zero, printing an appropriate message for each result.

    Hint: Your function signature is func safeDivide(_ a: Double, by b: Double) -> Result<Double, MathError>. Use guard b != 0 else { return .failure(.divisionByZero) } at the top.
    Deepen Your Understanding Use AI as a tutor — no code generation required
    Explain Swift's Result type to me without writing any code. How does it relate to the enum associated values I just learned? Walk me through how it works under the hood as if it were just a regular enum I wrote myself.
    When should I use Result versus a throwing function with try/catch? What are the practical situations where one is clearly better than the other? Give me concrete examples without writing the code.
    Build a Practice Example Get commented code you can study and learn from
    Write a Swift function that simulates fetching a user profile by ID and returns a Result type. If the ID is valid (greater than 0), return a success with a made-up username. If not, return a failure with a custom error enum. Then show how to handle the result with both a switch statement and .get(). Add a comment on every single line explaining what it does and why — write for someone who has never seen Result before.
    Stage complete: Once you've finished the challenge for this lesson, go back and look at the error handling patterns from Stage 10 again. You'll notice they fit together with what you've learned here — Result is the bridge between enums and error handling, and now you understand both sides.

    Stage 11 Recap: Enums in Depth

    Six lessons in, and Swift enums have gone from a simple labelling tool to one of the most expressive features in the language. Here's what you covered:

    • Lesson 11.1 — Basic Enums Revisited: Enums define a fixed set of possible values, work naturally with switch exhaustiveness, and eliminate whole categories of bugs that string and integer comparisons can't prevent.
    • Lesson 11.2 — Associated Values: Each enum case can carry different data at runtime — the feature that separates Swift enums from what you've seen in other languages, and the foundation for nearly every pattern in this stage.
    • Lesson 11.3 — Raw Values: Fixed underlying types (String, Int, etc.) let you map enum cases to external values like API keys and numeric codes, with CaseIterable giving you a free allCases collection.
    • Lesson 11.4 — Enums with Methods and Properties: Computed properties and methods live directly on the enum, keeping behavior close to the type that owns it and eliminating scattered switch statements across your codebase.
    • Lesson 11.5 — Using Enums to Model State: The idle/loading/loaded/failed pattern is the professional standard for state-driven SwiftUI development — you now have the vocabulary to read and write real app code that uses it.
    • Lesson 11.6 — The Result Type: Swift's built-in two-case enum for success and failure bridges the gap between error handling and associated values, and is especially common in completion handler and callback-based APIs.

    If you skipped any of the challenges, go back and do them now. The concepts in this stage build on each other and the challenges are where the patterns become muscle memory. Associated values especially — they're much stickier after you've written them yourself.

    Stage 12 is Concurrency — async/await, structured concurrency, and actors. It's the last major Swift language concept before you move into SwiftUI full time, and the state patterns you learned in Lesson 11.5 will connect directly to how async/await drives UI updates.