Lab 1.A — Playground exploration

Goal: Build muscle memory with the core Swift idioms — optionals, guards, Codable, async/await — by typing and breaking code in a Swift playground, then by fixing intentionally-broken samples, and finally by composing a small word-processing pipeline.

Time budget: 60–90 minutes.

Prerequisites: Xcode 16+ installed. Read chapters 1.1, 1.2, 1.3, 1.4, and 1.5.

Part 1 — Set up

  1. Launch Xcode → File → New → Playground → macOS → Blank.
  2. Save it as SwiftFundamentals.playground somewhere you’ll find it again.
  3. Delete the boilerplate. You should have an empty editor and a results sidebar on the right.

Part 2 — Exercises (type these, don’t paste)

2.1 Optionals and the five unwraps

let raw: String? = "42"

// (a) Force-unwrap — when do you allow yourself to do this?
let forced = raw!

// (b) if let
if let s = raw, let n = Int(s) { print("parsed", n) }

// (c) guard let — write a function `parseAge(_ s: String?) -> Int?`
//     that returns nil unless s is non-nil AND parses as Int.

// (d) nil-coalesce — show 5 different default values
let display = raw ?? "—"

// (e) Optional chaining — make `raw?.count` print; then chain it with
//     `?.description` so you end up with `String?`.

Write all five forms in your playground for the same raw. Notice how each changes the type of the result.

2.2 Guard and early-return

Rewrite this nested-if mess as a guard-based function with early returns:

func process(_ input: String?) -> String {
    if let s = input {
        if !s.isEmpty {
            if let n = Int(s) {
                if n > 0 {
                    return "got positive int \(n)"
                } else {
                    return "non-positive"
                }
            } else {
                return "not a number"
            }
        } else {
            return "empty"
        }
    } else {
        return "nil"
    }
}

The rewritten version should be readable top-to-bottom with no nesting deeper than 1 level.

2.3 Codable round-trip

struct User: Codable {
    let id: Int
    let name: String
    let createdAt: Date
}

let json = """
{ "id": 1, "name": "Ada", "createdAt": "2024-06-12T10:00:00Z" }
""".data(using: .utf8)!

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let user = try decoder.decode(User.self, from: json)
print(user)

Then make it fail. Change "id": 1 to "id": "one". Catch the DecodingError and print which field failed.

2.4 async/await in a playground

import Foundation

func slowGreeting(_ name: String) async -> String {
    try? await Task.sleep(for: .seconds(1))
    return "Hello, \(name)"
}

Task {
    let s = await slowGreeting("World")
    print(s)
}

You may need to enable indefinite execution in the playground (the Editor → Execute Playground menu). Note that the Task { … } runs after the surrounding synchronous code completes.

2.5 (Stretch) Macros — touch one Apple macro

@Observable                  // built-in Apple macro
final class Counter {
    var value = 0
    func inc() { value += 1 }
}

Right-click the @Observable and “Expand Macro” to see the generated code. You don’t have to write a macro in this lab, just observe how much code one annotation generated.

Part 3 — Fix the broken code

Each snippet below has at least one bug. Fix each in place and explain why it was broken in a comment.

// (a)
let count: Int = "10"          // type mismatch

// (b)
var maybeName: String? = nil
print("Hello, " + maybeName)   // optional in string concatenation

// (c)
func divide(_ a: Int, by b: Int) -> Int {
    return a / b               // crashes when b == 0
}
let r = divide(10, by: 0)

// (d)
let words = ["one", "two", "three"]
let upper = words.map { word in
    print("upper: \(word)")
    word.uppercased()           // forgot return — closure type confusion
}

// (e)
class Counter {
    var n = 0
    func inc() { n += 1 }
}
let c = Counter()
c.n = 5
// Why does this work but `let n = Int(); n = 5` doesn't?

Part 4 — Word-processing pipeline (capstone)

Given the multi-line string:

let corpus = """
The quick brown fox jumps over the lazy dog.
Pack my box with five dozen liquor jugs.
How vexingly quick daft zebras jump!
"""

Write one expression (one chained sequence of higher-order calls — split, map, filter, reduce, etc.) that produces a [String: Int] of [word: count], case-insensitive, ignoring punctuation, only words of length ≥ 4.

Expected result (order doesn’t matter):

["quick": 2, "brown": 1, "jumps": 1, "over": 1, "lazy": 1, "pack": 1,
 "with": 1, "five": 1, "dozen": 1, "liquor": 1, "jugs": 1, "vexingly": 1,
 "daft": 1, "zebras": 1, "jump": 1]

Hints:

  • String.components(separatedBy: .punctuationCharacters) strips punctuation.
  • Dictionary(grouping: by:) then .mapValues(\.count) is a clean way to count.
  • Or use reduce(into:_:) with a [String: Int] accumulator.

Done when

  • You can rewrite a 5-level-nested if let chain as guard-and-return without thinking.
  • You’ve seen a DecodingError and read its userInfo.
  • You’ve run an async function in a playground and understand why Task { } is needed.
  • You can fix all five broken snippets in Part 3 in under 3 minutes.
  • Your word-counter pipeline in Part 4 is a single expression that fits on three lines.

Stretch goals

  • Make User (from 2.3) conform to Identifiable and Hashable. Add it to a Set. Try to add a duplicate.
  • Implement a Result<User, DecodingError> return type for your decoder wrapper.
  • Replace the synchronous Counter with an actor Counter. Notice every call site now needs await.

Next lab: 1.B — Command-line tool with SwiftPM