iOS / Swift / Swift values and SwiftUI ownership / Lesson 3
SwiftUI state, bindings, and identity
Treat views as descriptions and give editable state one owner that survives the intended view identity.
On this page
Lesson goals
- A View value describes the current interface
- Keep local source of truth private
- A Binding grants access to another owner
A View value describes the current interface
A SwiftUI View is commonly a struct whose body describes an interface for current inputs and state. SwiftUI can recreate view values frequently while preserving managed state for a stable identity. Do not treat a struct initializer or body evaluation as a once-only screen lifecycle event. Sending a request or registering an observer there can repeat work unexpectedly. Keep body a description and place actions at event or lifecycle boundaries designed for them. The framework’s internal evaluation count is not a reliable count of user intent, saves, or displayed frames.
Keep local source of truth private
@State declares storage managed by SwiftUI for a value local to a view identity. Mark owned state private so callers do not accidentally treat its initial value as a normal continuously controlled input. If a parent must own the value, the child should receive a read-only input or a Binding according to its authority. The first fragment toggles a local disclosure flag and derives both the button label and details from it. There is no second stored string that can disagree with the flag. Exact state implementation details can evolve across Xcode releases, while ownership remains the key contract.
import SwiftUI
struct DetailsPanel: View {
@State private var open = false
var body: some View {
VStack {
Button(open ? "Hide" : "Show") { open.toggle() }
if open { Text("Workshop details") }
}
}
}
Initially Show; activation changes the label to Hide and reveals Workshop details.
SwiftUI owns storage for the view identity. This is an unexecuted View module requiring an Apple application or preview host.
A Binding grants access to another owner
A Binding is a read-and-write connection to a value owned elsewhere; it does not create an independent copy of that source of truth. The second fragment receives a binding for a preference and toggles it. The parent passes the projected state with a dollar prefix. This contract is appropriate when the child is allowed to edit that exact value. For a consequential action requiring validation, a command callback may be a better boundary than an unrestricted binding. Avoid letting a decorative control bypass domain rules by directly writing a deeply nested model property.
struct FavoriteToggle: View {
@Binding var favorite: Bool
var body: some View {
Toggle("Favorite", isOn: $favorite)
}
}
struct FavoriteHost: View {
@State private var favorite = false
var body: some View { FavoriteToggle(favorite: $favorite) }
}
The child edits the same favorite value owned by FavoriteHost.
The dollar projection supplies a binding. Passing favorite without the projection would supply a Bool, which is a different input type.
Identity determines the managed state lifetime
A view’s type, structural position, and explicit identity can affect whether SwiftUI preserves its state. Replacing a branch with a different identity may reset local drafts even when it looks similar on screen. An explicit .id is therefore a lifecycle decision, not a harmless decoration. Do not use a newly generated UUID on every body evaluation for an item that should remain the same. For repeated rows, use stable domain identifiers rather than array positions. Test an actual reorder or conditional transition with an edited field to detect unintended state migration or loss.
Separate local editing from shared models
A draft that can be cancelled should not necessarily bind directly into the shared saved model. Create a deliberate editable value, then validate and commit it on Save. A simple preference that takes effect immediately can use a direct binding when that is the product contract. For broader shared reference models, Observation provides tracked dependencies on supported OS versions; the view must read the relevant observable properties. Observation is a notification mechanism, not an authorization or resource-lifetime policy. Choose ownership and actor isolation explicitly before selecting property wrappers.
Verify state through interaction and reconstruction
A useful check edits a value, triggers an unrelated parent update, and confirms the intended state persists. Then deliberately change identity and verify whether reset is expected. Test two child controls sharing one binding to ensure they reflect the same source. For a cancellable draft, verify Cancel leaves the saved model unchanged and Save commits exactly once through its command. These tests reveal ownership defects that a static preview cannot. A preview is useful for presentation but does not establish the full navigation, lifecycle, or persistence contract.
Common errors and fixes
A draft resets after an unrelated update
- Why it happens
- The view identity changed or ownership was placed in a transient value.
- How to fix it
- Keep state under the intended stable owner and review explicit id changes.
- Prevent it next time
- Test parent updates while a field is edited.
A child needs Binding but receives Bool
- Why it happens
- The parent passed a value rather than its projected connection.
- How to fix it
- Pass the dollar-projected binding when write authority is intended.
- Prevent it next time
- Make read-only versus editable inputs explicit.
Cancel has already changed saved data
- Why it happens
- The form bound directly to the authoritative model.
- How to fix it
- Edit a separate draft and commit through Save.
- Prevent it next time
- Define cancellation before choosing binding scope.
Check your understanding
1. Should a total derived from selected items have separate mutable State?
Show worked solution
Usually derive it from the authoritative items so the total cannot diverge. If caching becomes necessary, give invalidation the same owner and include all dependencies. Independent mutable copies create synchronization work that the property wrapper cannot solve automatically.
2. Why can adding .id(UUID()) repeatedly erase a form draft?
Show worked solution
It supplies a different explicit identity each evaluation, asking SwiftUI to treat the subtree as a different occurrence. Managed state associated with the old identity need not survive. Use stable identity unless resetting is the deliberate behavior and verify that transition.
Key takeaways
- A View value describes the current interface
- Verify state through interaction and reconstruction