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
- Launch Xcode → File → New → Playground → macOS → Blank.
- Save it as
SwiftFundamentals.playgroundsomewhere you’ll find it again. - 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 letchain as guard-and-return without thinking. -
You’ve seen a
DecodingErrorand read itsuserInfo. -
You’ve run an
asyncfunction in a playground and understand whyTask { }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 toIdentifiableandHashable. Add it to aSet. Try to add a duplicate. - Implement a
Result<User, DecodingError>return type for your decoder wrapper. - Replace the synchronous
Counterwith anactor Counter. Notice every call site now needsawait.
Next lab: 1.B — Command-line tool with SwiftPM