Articles
SwiftUI State Management Interview Questions: Ownership, Identity, and Observation
Prepare for SwiftUI state management interviews with tested ownership and identity scenarios, Observation migration examples, and persistence boundaries.
SwiftUI state management interview questions become easier when you separate three decisions: who owns the model, which view identity keeps its storage alive, and how data survives after the process ends. Choosing a property wrapper before answering those questions often produces a form that works until someone changes a branch or navigates away.
This guide follows an original editing-form experiment, compiled with Swift 6.3.3 and run on macOS 26.4. Apple documentation supplies the framework contracts; the observed results below come from our small native app. These are preparation exercises, not claims that every employer asks this round.
Who Owns the Model in This SwiftUI Form?
Imagine an application form with a profile page and a review page. The applicant enters a name, visits the review, and returns to edit. Both pages must reference the same draft. A page can disappear while the editing session continues, so giving each page its own fresh model is the wrong lifetime for this requirement.
Our design recommendation is to place the draft in their common editing-session owner. Pass the model down to readers and editors. Keep page-specific presentation state, such as an expanded help panel, inside that page. The model's scope should follow the product's required lifetime, rather than whichever view happens to be easiest to modify.
Apple's StateObject documentation describes stable storage for an observable reference model during the declaring container's lifetime. In the older observation system, the owner uses @StateObject; a child receiving that model normally uses @ObservedObject. The latter observes the supplied instance but does not establish the same persistent owner storage.
This distinction is about SwiftUI data flow. It does not mean that an observed property has no ordinary strong reference, or that @ObservedObject inherently leaks memory. Explain reference retention separately from the framework's ownership convention.
| Requirement | ObservableObject approach | Observation approach |
|---|---|---|
| Create the session's model in a stable owner | @StateObject private var draft | @State private var draft |
| Read an injected model | @ObservedObject var draft | A stored model reference can be sufficient |
| Edit a model property through a control | Project a binding from the observed object | Use @Bindable to project a binding |
| Keep a local Boolean or selection | @State | @State |
| Restore edits after termination | Add an explicit persistence strategy | Add an explicit persistence strategy |
Do not mechanically replace every wrapper across a codebase. First identify which observation system the model actually uses and the deployment targets you must support.
Compare Two Minimal Owner-and-Editor Implementations
Here is the first half of the compiled experiment. The UUID makes the retained model visible, while the text field exposes whether input survives a transition. @MainActor keeps these UI model examples on the main actor; the exercise does not explore background synchronization.
import SwiftUI
@MainActor final class LegacyDraft: ObservableObject {
let id = UUID()
@Published var name = ""
}
@MainActor struct LegacyOwner: View {
@StateObject private var draft = LegacyDraft()
var body: some View { LegacyEditor(draft: draft) }
}
@MainActor struct LegacyEditor: View {
@ObservedObject var draft: LegacyDraft
var body: some View {
VStack {
Text(draft.id.uuidString)
TextField("Name", text: $draft.name)
}
}
}
Notice that the child receives draft. It does not initialize a second LegacyDraft when it receives the parent's model. If both pages instead create their own owners, each page can start with an empty name even though both models have the same type.
Apple's Observation migration guide documents SwiftUI integration beginning with iOS 17 and macOS 14. The corresponding version of our model removes ObservableObject and @Published, adopts @Observable, and moves the owner's storage to @State.
import SwiftUI
import Observation
@Observable @MainActor final class ModernDraft {
let id = UUID()
var name = ""
}
@MainActor struct ModernOwner: View {
@State private var draft = ModernDraft()
var body: some View { ModernEditor(draft: draft) }
}
@MainActor struct ModernEditor: View {
@Bindable var draft: ModernDraft
var body: some View {
VStack {
Text(draft.id.uuidString)
TextField("Name", text: $draft.name)
}
}
}
The key migration is not simply fewer annotations. The owner still decides lifetime, the child still edits the same instance, and the text field still needs a two-way binding. A successful explanation identifies all three responsibilities in the code.
Why Did the Form State Reset?
Before reading the results, predict what happens after entering a name and performing each action: update an unrelated parent counter, remove and restore the owner branch, change an explicit identity, or quit and reopen the app.
Our harness placed both owner views inside one conditional subtree. Its parent had independent counter, visibility, and identity values. The relevant structure was:
if show {
VStack {
LegacyOwner()
ModernOwner()
}
.id(identity)
} else {
Text("Owners removed")
}
Changing the counter reevaluated the parent without deliberately replacing this subtree's identity. Toggling show removed the owners, then inserted them again. Incrementing identity intentionally replaced the subtree's explicit identity.
| Action in the native experiment | Legacy model | Modern model | Input result |
|---|---|---|---|
| Parent counter changes from 1 to 2 | Same displayed UUID | Same displayed UUID | Ada retained |
| Remove owners, then restore them | New UUID | New UUID | Empty |
Enter Grace, then change .id | New UUID | New UUID | Empty |
Enter Lin, quit, then reopen | New UUID | New UUID | Empty |
These are observed macOS results from the compiled harness, not an iOS simulator test. The experiment checks the retained model and visible input; it does not count every temporary initialization, body evaluation, or pixel redraw.
Apple's Demystify SwiftUI session explains why identity matters to lifetime. A fresh view value does not automatically mean fresh state storage. Conversely, preserving the same Swift type in source code does not guarantee that state survives removal from the hierarchy.
For the two-page form, move the owner above the page-switching branch and pass its model into each page. Switching the child then leaves the session owner intact. That is a design consequence of the documented lifetime model; our recorded experiment specifically tested replacement of the owners themselves.
Does StateObject Initialization Follow Changing Inputs?
A useful follow-up introduces an account or document identifier into the owner's initializer. Suppose the initial model is created from documentID. If the parent later supplies a different ID while the owner's identity remains stable, do not assume the existing state object is automatically replaced.
Apple documents that the state object's initialization closure establishes its initial value for that lifetime. Re-running the view initializer with another argument is different from creating a new owner identity. An answer that promises automatic synchronization can silently keep editing the previous document.
There are two legitimate designs, depending on the requirement. You can deliberately update the existing model to represent the new document, including a clear loading and cancellation policy. Or you can tie the editing subtree's identity to the document so that changing documents starts a new session.
The second choice also resets other state inside that boundary. It may discard unsaved text, focus, or selection. Explain that consequence before recommending .id(documentID) as a fix. Adding .id(UUID()) during repeated evaluation is especially suspicious: the identifier is no longer a stable statement of which session the UI represents.
For debugging, write down the old and new document IDs, the visible model UUID, and whether the owner branch remained mounted. Those observations distinguish stale initialization assumptions from accidental owner replacement.
What Do Observable and Bindable Actually Change?
Apple's migration documentation describes property-access-based observation: SwiftUI tracks observable properties read while evaluating a view's body. A change to a property that this body does not read is not, by itself, the same dependency as a change to the displayed name.
In the older model, publishing another property can notify a view observing that object even when the view does not display that particular property. This is a reason to understand the observation boundary, not a license to promise an exact number of evaluations or a universal performance improvement.
As a thought exercise, add backgroundCounter to the modern model but never read it in the editor's body. Contrast changing that property with changing name. Then move a displayed calculation into a computed property that reads name; the underlying access still matters. Label this as a dependency prediction unless you instrument and run it.
@Bindable solves another problem: producing bindings to mutable properties on an observable object. It does not replace the owner, save the object to disk, or make the model observable by itself. A read-only child can receive an observable model without adopting a binding wrapper solely to display text.
When an interviewer asks whether every child should use @Bindable, ask whether that child needs $model.property for a control. A label and an editable text field have different needs even when they receive the same object.
Does Keeping a Model Alive Persist the Draft?
The relaunch test intentionally included no persistence. Both observation systems correctly started with new models and empty strings. A state wrapper manages the live UI's state; it is not an implicit file format or database transaction.
For a real application form, define what should survive: a selected tab, a partially completed draft, or a submitted record. Those requirements should not all share an accidental storage mechanism. Apple's AppStorage reflects values in UserDefaults; it can suit small preferences, but its existence does not mean an entire domain model should be squeezed into a preference key.
Our engineering recommendation for a substantial draft is an explicit repository or persistence layer with stable record identity. Decide when to save, how to restore, and what to show if loading fails. Do not treat a newly generated diagnostic UUID as the durable document identifier.
The interview trade-off is concrete: saving every edit minimizes the unsaved window but requires appropriate write behavior; saving only on a final button risks losing unfinished work. State the product requirement before choosing either. No persistence implementation or crash-recovery guarantee was tested in this article.
Debug a Reset Before Changing Wrappers
Suppose a reviewer reports that typing works, but moving from profile to review loses the name. Start by comparing the model UUID on both pages. Different UUIDs suggest separate models or a replaced owner. Matching UUIDs with different displayed text suggest a different problem, such as a local copy initialized from the model and never written back.
Next inspect the smallest enclosing conditional and any explicit IDs. A branch that removes the owner deserves attention before you change observation annotations. If the owner stays mounted, inspect assignments that clear the name and code that loads an older saved record.
Finally describe one regression check: enter a distinctive value, visit review, return, and assert the value remains. Add a separate check for intentionally starting a new draft, which should reset it. Keeping every value forever is not correctness either. The required session boundary determines when preservation is desirable and when clearing is the expected behavior.
Practice Explaining the State Boundary
Use the prompts below to make your answers more specific than wrapper definitions.
| Practice question | State-management follow-up |
|---|---|
| Implement an iOS scrollable grid with navigation | Which owner preserves selection as screens change? |
| Implement tap-to-infect color grid on iOS | Where do the grid and undo history live? |
| Build an emoji blaster animation on iOS | Which state belongs to transient effects rather than the session? |
| Implement A Mobile Chat Interface In An Existing Codebase | What should happen to an unsent draft when the conversation changes? |
| Explain iOS ARC and avoid retain cycles | Separate reference retention from SwiftUI identity and storage. |
A useful answer starts with the required lifetime, points to the owner in code, predicts what identity changes will do, and names any persistence boundary. Return to the grid-with-navigation exercise, add an editable detail screen, and explain precisely which edits should survive returning to the grid.