cswork / libraryOpen practice workspace

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.

RequirementObservableObject approachObservation approach
Create the session's model in a stable owner@StateObject private var draft@State private var draft
Read an injected model@ObservedObject var draftA stored model reference can be sufficient
Edit a model property through a controlProject a binding from the observed objectUse @Bindable to project a binding
Keep a local Boolean or selection@State@State
Restore edits after terminationAdd an explicit persistence strategyAdd 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 experimentLegacy modelModern modelInput result
Parent counter changes from 1 to 2Same displayed UUIDSame displayed UUIDAda retained
Remove owners, then restore themNew UUIDNew UUIDEmpty
Enter Grace, then change .idNew UUIDNew UUIDEmpty
Enter Lin, quit, then reopenNew UUIDNew UUIDEmpty

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 questionState-management follow-up
Implement an iOS scrollable grid with navigationWhich owner preserves selection as screens change?
Implement tap-to-infect color grid on iOSWhere do the grid and undo history live?
Build an emoji blaster animation on iOSWhich state belongs to transient effects rather than the session?
Implement A Mobile Chat Interface In An Existing CodebaseWhat should happen to an unsent draft when the conversation changes?
Explain iOS ARC and avoid retain cyclesSeparate 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.

Sources and Further Reading