These are the lesson notes for this day. Its build-along tutorial and flash cards have not been written yet.
SwiftUI has to decide whether a view in the new description is the same view as one in the old description. That is the view's identity. By default identity is structural: it comes from the view's position in your code. Views inside a ForEach take their identity from the id of their data. You can also set identity yourself with .id(). Identity decides lifetime. While a view keeps its identity, its @State is preserved and changes to it animate smoothly. When the identity changes, SwiftUI destroys the old view, along with its state, and creates a new one.
This explains most behavior that seems strange at first. The two branches of an if/else are two different views, so switching between them resets their state and produces a fade rather than a smooth change. Where you can, prefer a single view with a value that changes — .opacity(isOn ? 1 : 0.5) — over two views in an if.
Put a TextField in each branch of an if/else and toggle the condition while typing: the text is lost. Fix it so that the text survives. Then use .id() to reset a view's state on purpose.
@State when its identity changes?if/else produce a fade rather than an animated change?“Tap the counter up to five, then flip the toggle. The count snaps back to zero. Why? And how would you write this in 2026?”
final class CounterModel: ObservableObject {
@Published var count = 0
}
struct CounterView: View {
@ObservedObject var model = CounterModel()
var body: some View {
Button("Count: \(model.count)") { model.count += 1 }
}
}
struct Screen: View {
@State private var isOn = false
var body: some View {
Toggle("Dark header", isOn: $isOn)
CounterView()
}
}
Where this comes from: Hacking with Swift: iOS interview questions · StateObject (legacy) — from Hacking with Swift's question list: “When would you use @StateObject versus @ObservedObject?”
@ObservedObject watches an object. It does not own one. Flipping the toggle re-runs Screen.body, which builds a new CounterView value, whose default expression builds a new CounterModel. The view kept its identity; the model did not survive. The older fix is @StateObject, which ties the object to the view's lifetime.
@Observable final class Counter {
var count = 0
}
struct CounterView2: View {
@State private var model = Counter() // the view OWNS it; created once
var body: some View {
Button("Count: \(model.count)") { model.count += 1 }
}
}In 2026 the pair to use is @Observable with @State: no @Published, no Combine, and a view re-renders only for the properties it actually reads. As of Xcode 27, @State initializes lazily, so Counter() runs exactly once.
What interviewers listen for: The difference between owning and observing, in those words — and the modern form, offered without being asked.