How it works
The explanation, with examples you can run.
Identity: which view is "the same" view
SwiftUI gives every view an identity in one of two ways:
- Structural: its position and type in the view tree. The two branches of an
ifare different views, even if both show aText. - Explicit: an
id, as inForEach(items, id: \.id)or.id(value).
State is tied to identity. As long as the identity stays, @State keeps its value however often the struct is recreated. When the identity ends (the branch disappears, or the .id changes), the state is destroyed and any .task attached to the view is cancelled. Give list rows stable ids taken from your data, not array indices or fresh UUID()s, or every reload throws the row state away.
Where truth lives
@Observable final class Library { // iOS 17+: the Observation framework var books: [Book] = [] var filter = ""} struct LibraryScreen: View { @State private var library = Library() // this view owns it var body: some View { BookList(library: library) // plain property: read-only use FilterField(library: library) }} struct FilterField: View { @Bindable var library: Library // to make bindings to its properties var body: some View { TextField("Filter", text: $library.filter) }} struct PlusOneButton: View { @Binding var count: Int // a value owned by someone else var body: some View { Button("+1") { count += 1 } }}@State: storage this view owns, for values or (since iOS 17) an@Observableobject. Keep itprivate. Note that the default value is created each time the view struct is initialised, even though SwiftUI keeps only the first one; avoid expensive work there and defer it to.task.@Binding: read and write access to a value owned by an ancestor.@Bindable: bindings to the properties of an@Observableobject you were handed.@Environment: values and objects passed down the tree without threading them through every initialiser, for example.environment(library)and@Environment(Library.self).
Invalidation: what triggers body
Before iOS 17, models adopted ObservableObject with @Published properties, and a view holding one (@StateObject, @ObservedObject) was invalidated when any published property changed. The @Observable macro, introduced with iOS 17, changed the rule: SwiftUI records which properties body actually reads and invalidates the view only when one of those changes. A list row that reads book.title doesn't redraw when book.isAvailable changes. Observation also tracks optionals and collections of objects, which ObservableObject couldn't.
Two practical consequences:
- Small views are cheaper. Splitting a big
bodyinto subviews narrows what each one depends on, so fewer of them run on each change. bodymust be fast and free of side effects. It can run often and at times you don't choose; start work in.taskor in response to events instead.
Both systems work side by side, so existing ObservableObject code can migrate one type at a time.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.