These are the lesson notes for this day. Its build-along tutorial and flash cards have not been written yet.
A macro generates Swift source code while your code is being compiled. An attached macro, written @Something, adds code to the declaration it is attached to. A freestanding macro, written #something, expands into code at the place it appears. The generated code is ordinary Swift, and Xcode will show it to you: right-click the macro and choose Expand Macro.
The macros you will meet in SwiftUI are @Observable, which rewrites a class so that changes to each property are tracked; #Preview, which declares a preview; @Model, which makes a class persistable with SwiftData; @Entry, which declares an environment value; and, since Xcode 27, @State. None of these is magic. When one of them behaves in a way you do not expect, expand it and read what it wrote.
@Observable
final class Trip {
var name = ""
var miles = 0
}
// Right-click @Observable and choose Expand Macro to see what it wrote:
// each property becomes a computed property that records reads and announces
// writes, backed by a hidden stored property. No @Published, no Combine.
Exercise
Mark a small class @Observable, expand the macro, and read every generated line. Do the same with a #Preview. Write down, in your own words, what each macro added.
@Observable add to a class?These are the other language changes since Swift 3, in the order they arrived. Codable converts types to and from JSON, and key paths such as \.name refer to a property as a value (Swift 4). CaseIterable lists an enum's cases, and the compiler writes Equatable and Hashable for you (4.2). The Result type carries a success or a failure (5.0). if let value { unwraps without repeating the name, and the standard library gained Regex, Duration and Clock (5.7). if and switch can be used as expressions that produce a value (5.9). A function can declare exactly which error type it throws (6.0).
Concurrency changed the most — async/await, actors, and Swift 6's compile-time checking for data races — and has its own part of this course (Part 9). Tooling changed as well: Swift Package Manager is now the standard way to add dependencies, in place of CocoaPods and Carthage.
Take about 200 lines of Swift you wrote years ago and modernize it: Codable instead of hand-written JSON parsing, if let x {, switch used as an expression, async/await instead of completion handlers. Note which change removed the most code.
Codable give you?if let name = name { in the short form introduced in Swift 5.7.“A colleague's helper refuses to build. Tell me why in one sentence. Then fix it three different ways, and say which one you would ship.”
func statusBadge(isUrgent: Bool) -> some View {
if isUrgent {
return Text("Urgent").bold()
} else {
return Label("Later", systemImage: "clock")
}
}
Where this comes from: Senior iOS Interview Preparation (GitHub) · SE-0244: Opaque Result Types
The one sentence: some View promises the compiler one concrete type, chosen by the function — and these two return statements hand back a Text and a Label. The compiler says exactly that: “function declares an opaque return type 'some View', but the return statements in its body do not have matching underlying types.”
// 1 — let the builder choose (ship this)
@ViewBuilder
func statusBadge1(isUrgent: Bool) -> some View {
if isUrgent {
Text("Urgent").bold()
} else {
Label("Later", systemImage: "clock")
}
}
// 2 — make both branches the same concrete type
func statusBadge2(isUrgent: Bool) -> some View {
Label(isUrgent ? "Urgent" : "Later",
systemImage: isUrgent ? "exclamationmark.triangle" : "clock")
}
// 3 — erase the type (last resort)
func statusBadge3(isUrgent: Bool) -> some View {
isUrgent ? AnyView(Text("Urgent").bold())
: AnyView(Label("Later", systemImage: "clock"))
}Ship the first. @ViewBuilder turns the if/else into a single type (_ConditionalContent), so the promise holds and SwiftUI keeps full type information. The second is best when both branches really are the same kind of thing. AnyView compiles, but it erases the type information SwiftUI uses to update efficiently, so use it last — and say so.
What interviewers listen for: That you can explain it plainly: the caller does not choose the type, the function does, and it must choose one. Then that you bring up the cost of AnyView without being asked.