iOS · 13 min read

SwiftUI interview questions and what they score

SwiftUI interview questions on identity, state ownership, Observation and performance, each with the answer that scores, the trap and the follow-up.

This guide is for iOS engineers whose loop includes SwiftUI. It gives twelve SwiftUI interview questions as they are asked, grouped by what they probe. For each one: what the interviewer is scoring, the answer that scores, the trap, and the follow-up. There is a worked example of the bug behind several of these questions, and at the end, how to practise with the list.

The non-obvious part first. A large share of the SwiftUI bugs that come up in interviews are identity bugs: a view that SwiftUI treats as new, so its state resets, its animation breaks or its task restarts. Candidates reach for property wrappers. Senior candidates name identity first, and then the wrapper.

Identity is the question behind the question

1. “What causes a view’s body to re-evaluate?”

What is scored: precision, because every performance answer later depends on it.

The answer that scores: “A change to its own @State, to an observed-object property that body reads, or to an environment value it reads; a different value handed down by the parent; or a new identity. That is why body stays cheap and side-effect free, reads the minimum, and identity does not change between updates.”

The trap: “when the data changes.” The follow-up is “which data, exactly?”, and now you are answering the question a second time.

2. “What is view identity, and how can it change without you noticing?”

What is scored: whether you know identity has two sources.

The answer that scores: “Structural identity comes from the view’s position in the body: an if and an else branch are two different views, even if they contain the same type. Explicit identity comes from id(_:) and from the ids in ForEach and List. When identity changes, SwiftUI treats it as a new view: @State is discarded, .task restarts, and transitions play instead of animations.”

The follow-up: “Where does AnyView fit in?” It erases the type, so SwiftUI loses structural information it would otherwise use. Prefer @ViewBuilder and concrete types.

3. “Why not use the array index as the id in a ForEach?”

What is scored: whether you have seen the bug.

The answer that scores: “Inserting, deleting or reordering shifts every index after the change, so SwiftUI thinks those rows changed identity. Rows rebuild, row state moves to the wrong item, and animations break. Use a stable, unique id from the model.”

Recovery line if you started with the index: “Index works only for a list that never changes. As soon as rows can be inserted, I need the model’s own id.”

State ownership

4. “@Observable or ObservableObject, and what is the difference?”

What is scored: whether you can sell the real win without overclaiming.

The answer that scores: “ObservableObject fires objectWillChange on any @Published change, so every observing view re-evaluates, even if it reads a property that did not change. With @Observable, available from iOS 17, a view depends only on the properties it reads in body, so usually fewer views re-evaluate. It does not prevent every re-evaluation: the view is still invalidated when its parent updates, its identity changes, or the environment changes.”

The trap: the interviewer asks “so only views that read the changed property update?” and waits for you to say yes. The senior answer says “usually, not only.”

The follow-up: “The app supports iOS 16.” Then ObservableObject stays for those targets, and you reduce its blast radius by splitting objects and passing values down.

5. “Who owns this model: @State, @Bindable, or a plain let?”

What is scored: ownership, which is the real question behind every wrapper.

The answer that scores: “The view that creates an @Observable model and should keep it for its lifetime holds it in @State. A child that only reads it takes a plain let. A child that needs bindings to its properties uses @Bindable. The older pairing is the same rule: @StateObject owns, @ObservedObject observes something owned elsewhere.”

The trap: creating an object with @ObservedObject var model = Model() inside a child. It is recreated every time the parent re-evaluates, and the state resets. It is a common SwiftUI lifecycle bug, which is why it is a reliable screen question.

6. “How do you decide where a piece of state lives?”

What is scored: architecture at the scale of a screen, and then an app.

The answer that scores: “By who owns the value. @State for what the view owns, @Binding for a parent’s value the child edits, the environment for shared dependencies, and a model for long-lived presentation state. Derived values are computed, not stored, so they cannot drift.”

The follow-up that tests it: “Typing in the search field makes the settings screen re-render. Why?” Because both observe one global app-state object, so a high-frequency change on one screen ripples everywhere. Scope state to features.

Lists, effects and navigation

7. “The list stutters. What do you check?”

What is scored: method, then the levers.

The answer that scores: “First I confirm what is re-evaluating: Self._printChanges() inside body during development, and the SwiftUI instrument in Instruments for body update counts. The usual causes are unstable ids, sorting, filtering or formatting inside body, formatters created per row, and images decoded on the main thread. List realises rows lazily and reuses them, so I prefer it for large data. .equatable() only where profiling shows an expensive row with stable inputs.”

The trap: “I would switch to LazyVStack.” Laziness does not fix heavy work in body or unstable identity.

8. “What does the .task modifier give you that onAppear with a Task does not?”

What is scored: precision about lifetime.

The answer that scores: “.task starts async work when the view appears and cancels it when the view disappears. .task(id:) also cancels and restarts it when the id value changes, which is a clean way to reload on a new search term. A Task started in onAppear is not tied to the view, so I would have to cancel it myself.”

The follow-up: “Where does that work run?” Wherever the view is isolated, which normally means the main actor, so anything expensive belongs on another executor.

The follow-up that separates levels: “What do you pass as the id?” The smallest value whose change means “load again”: the query string, the detail id. Pass the whole model and the task restarts every time an unrelated field on it changes. Then the reverse case: “The user taps Pay and dismisses the sheet.” Now .task is the wrong owner, because the sheet going away would cancel the payment halfway. Anything that has to finish after the screen is gone, a payment, an upload, saving a draft, runs in a feature store or an app-level service, and the view just shows its progress. At senior level you name the owner of each piece of async work first and pick the modifier second.

9. “How do you handle navigation?”

What is scored: whether navigation is data you own.

The answer that scores: “NavigationStack bound to a path I own, kept in a router the feature owns rather than in a view model. Navigation becomes data: I append destinations, a deep link builds a path, and routing can be unit-tested without a UI.”

Dependencies, interop and proof

10. “How do you inject dependencies into views and keep them testable?”

The answer that scores: “Through the environment, with the key typed to a protocol rather than a concrete service, so previews and tests can inject a fake. For view model dependencies I still prefer initialiser injection of a repository protocol. That keeps everything replaceable without a DI framework.”

The follow-up: “Swift 6 complains about the default value.” The default is a shared static, so the protocol needs to be Sendable.

11. “When do you mix UIKit into a SwiftUI app, and how?”

What is scored: whether your answer comes from shipped work or from opinion.

The answer that scores: “Most real apps are hybrids. UIViewRepresentable and UIViewControllerRepresentable bring UIKit into SwiftUI, for example a camera controller or a text view that needs fine selection control. UIHostingController puts SwiftUI screens inside a UIKit app, which is how most migrations start. The boundary is where I am careful about who owns state and when the coordinator is updated.” The UIKit side of that boundary is in UIKit interview questions.

12. “How would you prove a view re-rendered, and why it did?”

What is scored: whether you measure before changing anything.

The answer that scores: “Self._printChanges() in body prints which dependency triggered the evaluation. The SwiftUI instrument shows how often each body runs under real use. I measure on a device in a release build, change one thing, and measure again.”

The trap: “I would add EquatableView.” A fix before a measurement is a guess, and interviewers write it down as one.

Worked example: the filter that keeps resetting

A bug report: “On the results screen, the text I typed in the filter disappears when I rotate to landscape.” The screen switches layout with a compact flag.

struct FilterPanel: View {
    @State private var draft = ""
    var body: some View { TextField("Filter", text: $draft) }
}

// Before: two branches, two identities. Toggling compact resets the draft.
struct ResultsHeaderBefore: View {
    let compact: Bool
    var body: some View {
        if compact {
            FilterPanel().font(.caption)
        } else {
            FilterPanel().font(.body)
        }
    }
}

// After: one view, a changing modifier. The draft survives.
struct ResultsHeaderAfter: View {
    let compact: Bool
    var body: some View {
        FilterPanel().font(compact ? .caption : .body)
    }
}

Both versions type-check in Swift 6 language mode (checked with Swift 6.3.2). In the first, the two branches of the if are different positions in the view tree, so SwiftUI sees a new FilterPanel when compact flips and throws away its @State. In the second there is one FilterPanel whose modifier changes, so its identity and its state survive.

A mid-level answer to this bug moves draft up to the parent. That works, and it hides the cause. The senior answer names identity, fixes the structure, and then asks whether the draft should live in the parent anyway because another view needs it. The order of those two sentences is what the interviewer is listening for.

How to use this list

Read a question, cover the answer, and say yours out loud in under thirty seconds. Then ask yourself the follow-up and answer that too. Where your first answer named a property wrapper, check whether identity or ownership was the real cause. The questions where your first answer was a definition and the follow-up had nothing behind it are the ones to work on.

If the deeper version of these questions is what you need, the concurrency and memory side of senior loops is in senior iOS interview questions. To choose a book for the rest of the loop, iOS interview books compares them by the time you have left.

What changed in 2024 to 2026

  • Swift 6.2 shipped on 15 September 2025 with what the Swift team calls approachable concurrency (Swift.org, Swift 6.2 released).
  • A module can opt in to main actor isolation by default, so its code runs on the main actor without @MainActor written on every type.
  • The new @concurrent attribute marks a function that should run concurrently, which makes it visible in the code when work leaves the actor.

Sources

What to do next

Free guide: the Phoenix Method