Skip to content
Mobile & design

8 iOS SwiftUI Interview Questions and Answers

This focused guide turns RecallDeck’s curated iOS SwiftUI material into 8 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

9 min read8 detailed answersReviewed Aug 24, 2026
What to remember

Tie implementation or design choices to platform behavior, user feedback, lifecycle, accessibility, and the trade-off the team must maintain.

Question set

8 detailed answers

01

@State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject: who owns the source of truth?

Short answer: With @State and @StateObject the view OWNS the source of truth: SwiftUI stores the value outside the struct, and it survives body recomputation. @Binding and @ObservedObject borrow someone else's state; @EnvironmentObject borrows it through the tree's environment.

In depth:

Wrapper Who owns it Survives parent re-render
@State the view (value type)
@StateObject the view (object)
@Binding the parent — (reference to borrowed state)
@ObservedObject outside code ✗ if created inline
@EnvironmentObject an ancestor via .environmentObject

The #1 interview trap:

struct ParentView: View {
    @State private var counter = 0
    var body: some View {
        Button("\(counter)") { counter += 1 }
        // ✗ a new VM on every recomputation of the parent's body
        ChildView(vm: ChildViewModel())
    }
}
struct ChildView: View {
    @ObservedObject var vm: ChildViewModel   // state gets lost
    // @StateObject var vm = ChildViewModel() — would be created once
}

Since iOS 17 the direction is the @Observable macro: ownership via @State, borrowing via a plain property or @Bindable; only the fields actually read are tracked.

⚠️ Common mistake: creating an @ObservedObject right in the view's initializer — every parent re-render recreates the object and wipes its state. The owner creates it with @StateObject; consumers receive it as @ObservedObject.

02

Why does the wrong id in ForEach break animations and row state?

Short answer: SwiftUI matches views across updates by identity. A stable id tells the framework "this is the same row, it moved" — the move animates and @State is preserved. An index-based id sticks to the position, not the data, on insert/remove: state and animations end up attached to the wrong rows.

In depth:

  1. Structural identity — position in the tree: the branches of an if/else are two different views; switching resets @State and plays a transition.
  2. Explicit identityForEach(_, id:) and .id(): the id must be stable for the lifetime of the data, not of a single render.
  3. How an index breaks it — after deleting the first row, "row 0" still exists but shows different data: its @State (say, isExpanded) has migrated to a neighbor, and instead of a move animation you get content jumping in place.
// ✗ id = index: state sticks to the position, not the data
ForEach(items.indices, id: \.self) { i in
    Row(item: items[i])
}

// ✓ stable id: SwiftUI sees the move and animates it
ForEach(items) { item in     // Item: Identifiable
    Row(item: item)
}

⚠️ Common mistake: minting identity on the fly — UUID() in a computed id or an unstable hash. Every update then looks like "delete everything + insert everything": state resets, move animations disappear, diffing degrades.

03

When does SwiftUI recompute body, and what does "body must be cheap" mean?

Short answer: body is recomputed when any observed dependency of the view changes: @State, @Binding, a @Published property of an observed object, an environment value. A "cheap" body is a pure description of the UI: no side effects, no heavy computation, no creation of objects with identity.

In depth:

  1. Triggers — a change to the view's own @State, an observed object's publisher, an environment value; with @Observable — only the fields actually read inside body.
  2. Purity — the framework is free to call body any number of times, whenever it wants; any side effect in it (analytics, writes, networking) fires an unpredictable number of times.
  3. Localization — split views into subviews: a dependency only touches the bodies that read it, and diffing updates a smaller piece of the tree.
// ✗ heavy work on every recomputation
var body: some View {
    let formatter = DateFormatter()          // allocation every time
    let sorted = items.sorted { $0.date > $1.date }
    return List(sorted) { ItemRow(item: $0) }
}

// ✓ formatter — static let, sorting — in the model,
//   body — description only

⚠️ Common mistake: "SwiftUI redraws the whole screen on any change." No: it recomputes the bodies of dependent views — which is cheap if body is pure; only the actual diff turns into pixels.

04

Why are SwiftUI Views structs, and why isn't constantly recreating them expensive?

Short answer: A View is not a view in the UIKit sense but a lightweight value-description: a blueprint of what to show. Structs are cheap to create and throw away, so SwiftUI freely recreates them on every body recomputation. The long-lived part — the render tree (attribute graph) — lives behind the scenes; that's where state and identity are kept.

In depth:

  1. Blueprint, not instance — a UIView is a heavyweight object with a lifecycle and a layer; a View struct is a couple of fields plus body, holding no resources.
  2. State lives outside — @State is stored by the framework and attached to the view's identity in the tree, not to the struct: the struct dies, the value survives.
  3. Diffing — SwiftUI compares the new description with the old one and minimally mutates the persistent tree; only the difference reaches the pixels.
body → View values (cheap, recreated every time)
            │ diff

attribute graph / render tree (long-lived:
@State, identity, animations)

⚠️ Common mistake: equating "recreating structs" with "redrawing the UI." Producing descriptions costs pennies; rendering is what can be expensive, and it happens only where the diff found a change.

05

How do you embed a UIKit view in SwiftUI: what do makeUIView, updateUIView and the Coordinator do?

Short answer: UIViewRepresentable (and UIViewControllerRepresentable) is the bridge into UIKit: makeUIView creates the view once, updateUIView syncs it with SwiftUI state on every update, and the Coordinator is a long-lived object for delegates and target-action. The reverse direction is UIHostingController.

In depth:

struct SearchField: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextField {
        let field = UITextField()            // created ONCE
        field.delegate = context.coordinator
        return field
    }
    func updateUIView(_ field: UITextField, context: Context) {
        if field.text != text {
            field.text = text                // sync only
        }
    }
    func makeCoordinator() -> Coordinator { Coordinator(text: $text) }

    final class Coordinator: NSObject, UITextFieldDelegate {
        var text: Binding<String>
        init(text: Binding<String>) { self.text = text }
        func textFieldDidChangeSelection(_ field: UITextField) {
            text.wrappedValue = field.text ?? ""
        }
    }
}
  1. make/update split — creation vs synchronization; the struct gets recreated, the UITextField and Coordinator live on.
  2. Coordinator — receives UIKit callbacks and writes back into SwiftUI through the Binding.

⚠️ Common mistake: configuring or recreating the view inside updateUIView — you'll reset UIKit state (cursor, scroll position) and pay extra work on every body recomputation.

06

SwiftUI or UIKit in 2026: how do you choose on a real project?

Short answer: Default to SwiftUI: development velocity, previews, a state-driven model. Reach for UIKit deliberately — where you need full control: complex collection layouts, custom gesture systems, fine-grained render work, old deployment targets. The mature answer is per-screen interop, not religion.

In depth:

Factor SwiftUI UIKit
New screen / app ✓ by default
Velocity, previews, iteration
Complex collection layouts, gestures hits walls in places ✓ full control
Fine render/animation control limited by API ✓ CALayer, display link
Old deployment target the older the iOS, the more it hurts ✓ any
Large existing UIKit codebase embeds via UIHostingController ✓ already there
  1. Screen by screen — UIHostingController and UIViewRepresentable let you mix the two worlds without rewriting the app.
  2. Team and codebase — the team's skills and existing code weigh more than framework ideology.
  3. Trend — Apple invests in SwiftUI (new APIs are often SwiftUI-first), so the long-term bet is obvious.

⚠️ Common mistake: a categorical "SwiftUI only" or "SwiftUI isn't ready." The interviewer is listening for factor-based judgment, not a slogan.

07

What do @Environment and @AppStorage give you, and how is .task different from onAppear?

Short answer: @Environment is dependency injection down the tree (system values and your own objects) without threading parameters through. @AppStorage is a @State-like wrapper over UserDefaults. .task ties async work to the view's LIFETIME: it's cancelled on disappear and restarts when its id changes; onAppear is a synchronous callback where you manage Tasks by hand.

In depth:

  1. @Environment — reads a value from the environment: \.dismiss, \.colorScheme, custom keys and @Observable objects; change it at an ancestor and every reader updates.
  2. @AppStorage — persistent state: writes go to UserDefaults, and an external change recomputes body.
  3. .task vs onAppear.task inherits actor context, auto-cancels and supports id:; onAppear is for synchronous things like analytics.
struct ProfileView: View {
    @Environment(\.dismiss) private var dismiss
    @AppStorage("theme") private var theme = "system"
    let userID: User.ID
    @State private var user: User?

    var body: some View {
        content
            // restarts when userID changes,
            // cancelled when leaving the screen
            .task(id: userID) {
                user = try? await api.loadUser(userID)
            }
    }
}

⚠️ Common mistake: onAppear { Task { … } } — that Task won't be cancelled when the screen goes away and will keep loading data for a dead view; .task gives you that for free.

08

Design the data flow for a screen: a list plus detail editing with save.

Short answer: One source of truth — an observable model at the list level (@StateObject or @Observable inside @State). The detail screen gets either a Binding (edits visible instantly) or a draft copy with an explicit Save — transactional editing. Loading lives in the list's .task; saving is an explicit model method.

In depth:

  1. Ownership — the list screen owns the store; the detail only borrows data and reports the result.
  2. Binding straight through — minimal code, but every keystroke is immediately visible in the list and the store: "Cancel" requires a manual rollback.
  3. Draft copy — a struct copy of the item in the detail's @State; Save hands it to the store, Cancel is free — the draft simply dies.
@Observable final class ItemsStore {
    var items: [Item] = []
    func load() async { /* fetch */ }
    func save(_ draft: Item) {
        // replace the item by id, push to the backend
    }
}

struct DetailView: View {
    @State private var draft: Item        // transactional copy
    let onSave: (Item) -> Void

    init(item: Item, onSave: @escaping (Item) -> Void) {
        _draft = State(initialValue: item)
        self.onSave = onSave
    }
    var body: some View {
        Form { TextField("Title", text: $draft.title) }
            .toolbar { Button("Save") { onSave(draft) } }
    }
}

⚠️ Common mistake: passing a Binding straight into the store and being surprised that unsaved edits show up in the list while "Cancel" cancels nothing. Transactional editing is a deliberate decision, not something you get by accident.

Source notes

References and review policy

RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS