SwiftUI
Declarative UI framework — state, bindings, and view composition.
18 questions
JuniorTheoryVery commonWhat is the difference between @State and Binding in SwiftUI?
What is the difference between @State and Binding in SwiftUI?
@State declares mutable view-local state owned by the view; mutating it re-renders the body. A Binding ($state) is a two-way reference letting a child read and write that value without owning it.
Common mistakes
- ✗Forgetting the
$prefix when passing state as a binding to a child view - ✗Believing
@Stateis shared across view instances rather than per-instance - ✗Storing reference-type models in
@Stateinstead of@StateObject
Follow-up questions
- →Why does SwiftUI re-render the body when a
@Statevalue changes? - →When would you reach for
@StateObjectinstead of@State?
MiddleDebuggingVery commononAppear fires twice — or never when you expect it. How does it differ from UIKit's lifecycle?
onAppear fires twice — or never when you expect it. How does it differ from UIKit's lifecycle?
onAppear follows the view's structural identity, not a controller lifecycle, and fires each time the view enters the hierarchy. A re-inserted view calls it again; one never inserted never fires. It is not a once-per-screen viewDidAppear.
Common mistakes
- ✗Treating
onAppearas a once-per-screenviewDidAppearequivalent - ✗Putting idempotency-sensitive setup in
onAppearwithout a guard - ✗Expecting
onAppearto fire for a view that is never inserted
Follow-up questions
- →Why does a re-identified view fire
onAppearagain? - →Where would you put load-once logic so a re-insertion doesn't repeat it?
MiddlePerformanceVery common.task vs .onAppear vs .onChange — where do you start async loading, and who cancels it?
.task vs .onAppear vs .onChange — where do you start async loading, and who cancels it?
Start it in .task: it runs when the view appears and its Task is cancelled on disappear. .onAppear is synchronous with no cancellation, so a Task there leaks or re-runs. .task(id:) reloads on change while keeping auto-cancel.
Common mistakes
- ✗Spawning an un-cancelled
Taskinside.onAppear - ✗Believing
.onChangefires on every re-render rather than on value change - ✗Not using
.task(id:)to reload while keeping automatic cancellation
Follow-up questions
- →Why is a Task started in
.taskcancelled when the view disappears? - →How does
.task(id:)reload without leaking the previous Task?
JuniorTheoryCommonHow does a view subscribe to an ObservableObject, and who owns it — @StateObject, @ObservedObject, or @EnvironmentObject?
How does a view subscribe to an ObservableObject, and who owns it — @StateObject, @ObservedObject, or @EnvironmentObject?
A view observes an ObservableObject by holding it in a @StateObject, @ObservedObject, or @EnvironmentObject. @Published fields re-render subscribed views on change. @StateObject owns it; the other two reference an object owned elsewhere.
Common mistakes
- ✗Thinking you must manually subscribe with
sinkinstead of just declaring a wrapper - ✗Believing
@ObservedObjectowns and keeps the object alive - ✗Confusing which wrapper owns the object versus references an external one
Follow-up questions
- →What does marking a property
@Publishedactually do to observing views? - →Which of the three wrappers keeps the object alive, and why does that matter?
JuniorTheoryCommonWhat does 'a view is a description, not an object' mean for onAppear and onDisappear?
What does 'a view is a description, not an object' mean for onAppear and onDisappear?
A view value is a description SwiftUI reads to build the render tree; the struct is made and discarded constantly with no lifecycle. onAppear/onDisappear fire when the render node enters or leaves the hierarchy, not at struct creation.
Common mistakes
- ✗Equating the struct's
initwith the view appearing on screen - ✗Thinking view structs are long-lived objects with a lifecycle
- ✗Expecting
onAppearto run once per app rather than per insertion
Follow-up questions
- →Why can the same view struct be created many times without appearing?
- →When exactly does
onDisappearfire relative to the render tree?
JuniorTheoryCommonWhy are SwiftUI views structs, what does some View mean, and how often is body re-evaluated?
Why are SwiftUI views structs, what does some View mean, and how often is body re-evaluated?
Views are lightweight structs — value types holding a description of the UI, not rendered objects, so SwiftUI creates and discards them constantly. some View is an opaque return type — one concrete View. body re-runs when state or inputs change.
Common mistakes
- ✗Thinking views are long-lived objects rather than transient value types
- ✗Reading
some Viewasany View(an existential) rather than one concrete type - ✗Assuming re-evaluating
bodyis expensive and must be avoided
Follow-up questions
- →Why is it cheap for SwiftUI to re-evaluate
bodymany times? - →How does
some Viewdiffer fromany View?
MiddleDebuggingCommonFix '@EnvironmentObject may be missing as an ancestor' — and how is @Environment different?
Fix '@EnvironmentObject may be missing as an ancestor' — and how is @Environment different?
A view read an @EnvironmentObject no ancestor injected via .environmentObject(_:). Inject it on the sheet or navigation destination that needs it — those don't inherit the presenter's environment. @Environment differs — a keyed value with a default.
Common mistakes
- ✗Not injecting the object into a sheet or navigation destination's environment
- ✗Confusing
@EnvironmentObject(injected object) with@Environment(keyed value) - ✗Making the property optional to silence the crash instead of injecting upstream
Follow-up questions
- →Why doesn't a presented sheet inherit the presenter's environment object?
- →Why can
@Environmentnever crash for a missing value the way@EnvironmentObjectdoes?
MiddleDebuggingCommonFix the 'Modifying state during view update' runtime warning
Fix the 'Modifying state during view update' runtime warning
It happens when body mutates observed state while SwiftUI is still computing that body — assigning a @State value during evaluation. Move the mutation out of the render pass into .onAppear, .task, .onChange, or a user action.
Common mistakes
- ✗Assigning
@State/@Publishedvalues directly insidebody - ✗Thinking
DispatchQueue.main.asyncaround the write fixes the root cause - ✗Treating the warning as harmless instead of a real re-entrancy bug
Follow-up questions
- →Why is mutating observed state during
bodyevaluation undefined? - →Which modifier would you use to run the mutation after the view appears?
MiddleTheoryCommonWhat breaks when a view creates its own model with @ObservedObject instead of @StateObject?
What breaks when a view creates its own model with @ObservedObject instead of @StateObject?
@StateObject makes the view own the model — SwiftUI creates it once and keeps it alive across re-renders. @ObservedObject only observes an externally-owned object, so creating it there rebuilds it fresh on every re-render, resetting its state.
Common mistakes
- ✗Believing
@ObservedObjectowns and persists the model like@StateObject - ✗Creating the model inline in a view that marks it
@ObservedObject - ✗Thinking the choice is cosmetic because both compile and often look fine
Follow-up questions
- →Why does
@StateObjectsurvive a parent re-render but a locally-created@ObservedObjectdoes not? - →When is
@ObservedObjectthe correct choice for a model?
MiddleCodeCommonRewrite a UIKit name-form view controller as SwiftUI
Rewrite a UIKit name-form view controller as SwiftUI
Replace the controller with a View holding @State for name and greeting. TextField("Name", text: $name) binds the input, a Button sets the greeting, and if !greeting.isEmpty { Text(greeting) } replaces isHidden. A VStack with padding replaces Auto Layout.
Common mistakes
- ✗Reaching for
UIViewControllerRepresentableinstead of writing a native SwiftUI view - ✗Toggling visibility with a flag instead of conditional view inclusion (
if ...) - ✗Keeping manual Auto Layout constraints rather than stack-based declarative layout
Follow-up questions
- →Why does
if !greeting.isEmptyremove a view rather than just hide it? - →How does
$namekeep theTextFieldand the model in sync without target-action?
MiddleTheoryOccasionalHow does the @Observable macro (iOS 17+) change which views get invalidated versus ObservableObject?
How does the @Observable macro (iOS 17+) change which views get invalidated versus ObservableObject?
ObservableObject re-renders every observing view whenever any @Published field changes. The @Observable macro (iOS 17+) tracks per-property reads, so a view is invalidated only when a property it read changes. Pair it with @State.
Common mistakes
- ✗Thinking
@Observableonly removes@Publishedand keeps whole-object invalidation - ✗Pairing
@Observablewith@StateObjectinstead of@State - ✗Believing
@Observableis a property wrapper rather than a macro
Follow-up questions
- →Why does per-property read tracking cut the number of view invalidations?
- →Why is
@Observablepaired with@Staterather than@StateObject?
MiddleDebuggingOccasionalList rows reset their state after a reorder because id is the array index
List rows reset their state after a reorder because id is the array index
Keying rows by array index ties identity to position, so after a reorder SwiftUI treats slot 0 as the same view and keeps its old state on new data. Give each element a stable Identifiable id so identity follows the data, not the slot.
Common mistakes
- ✗Using the array index (an offset) as the row's identity
- ✗Believing SwiftUI tracks rows by contents rather than by the supplied
id - ✗Adding
.id(UUID())to force a rebuild instead of giving stable identity
Follow-up questions
- →What is the difference between structural identity and explicit
.id()identity? - →Why does a stable
Identifiableid preserve per-row@Stateacross a move?
MiddleCodeOccasionalWrite a custom container view that takes a result-builder @ViewBuilder closure
Write a custom container view that takes a result-builder @ViewBuilder closure
Make a generic struct storing content: Content (Content: View) and mark its init closure @ViewBuilder content: () -> Content. Store the result and place content in your layout. The attribute lets callers pass several views as one closure.
Common mistakes
- ✗Typing the closure as
[AnyView]instead of a generic@ViewBuilderclosure - ✗Forgetting
@ViewBuilder, so only a single child view compiles - ✗Making the content closure
@escapingand re-invoking it per render
Follow-up questions
- →Why does
@ViewBuilderlet the caller pass several views without commas or an array? - →What concrete type does the builder produce for multiple children?
SeniorPerformanceOccasionalA List of 10 000 rows stutters. What does SwiftUI create lazily, and what defeats that laziness?
A List of 10 000 rows stutters. What does SwiftUI create lazily, and what defeats that laziness?
List and LazyVStack build only the rows near the viewport and reuse them while scrolling, so a huge collection stays cheap. Laziness breaks when rows are erased to AnyView, wrapped in a plain non-lazy VStack, or given an expensive body.
Common mistakes
- ✗Wrapping row views in
AnyViewand defeating reuse and diffing - ✗Using a plain
VStack/ScrollViewinstead ofList/LazyVStack - ✗Doing expensive work (formatting, decoding) inside a row's
body
Follow-up questions
- →Why does
AnyViewprevent SwiftUI from reusing a row view? - →How does a
LazyVStackdiffer from a plainVStackfor large data?
SeniorPerformanceOccasionalA form re-renders the whole screen on every keystroke because the whole model is observed. How do you narrow the invalidation?
A form re-renders the whole screen on every keystroke because the whole model is observed. How do you narrow the invalidation?
Adopt the @Observable macro so SwiftUI tracks per-property reads and re-renders only what changed. Split the model into smaller observable pieces and put each field in its own subview, making that body the invalidation unit.
Common mistakes
- ✗Keeping one giant
ObservableObjectobserved whole from a single view - ✗Reaching for
debounceinstead of narrowing what is observed - ✗Assuming
AnyViewcaching reduces the number of re-renders
Follow-up questions
- →Why does extracting a field into its own subview limit the re-render to that field?
- →How does
@Observableavoid invalidating views that never read a property?
SeniorDesignOccasionalYou need to embed a UIKit MKMapView in a SwiftUI screen. Annotations are driven by SwiftUI state, and when the user pans the map or taps a pin, the surrounding SwiftUI view must update (for example a selected-place detail panel). Describe how you bridge the map with the interop protocol UIViewRepresentable: what makeUIView and updateUIView each do, what the Coordinator is for, and how you keep state flowing both ways — SwiftUI state into the map, and the map's delegate callbacks (region changes, pin taps) back into SwiftUI — without feedback loops. Note the ownership and lifetime of the MKMapView and the Coordinator.
You need to embed a UIKit MKMapView in a SwiftUI screen. Annotations are driven by SwiftUI state, and when the user pans the map or taps a pin, the surrounding SwiftUI view must update (for example a selected-place detail panel). Describe how you bridge the map with the interop protocol UIViewRepresentable: what makeUIView and updateUIView each do, what the Coordinator is for, and how you keep state flowing both ways — SwiftUI state into the map, and the map's delegate callbacks (region changes, pin taps) back into SwiftUI — without feedback loops. Note the ownership and lifetime of the MKMapView and the Coordinator.
Wrap the map in a UIViewRepresentable: makeUIView builds the MKMapView once; updateUIView pushes new state into it. The Coordinator holds its delegate and forwards callbacks — region changes, pin taps — back to SwiftUI via @Binding, guarding the write-back so it doesn't loop.
Common mistakes
- ✗Recreating the
MKMapViewinupdateUIViewinstead of once inmakeUIView - ✗Omitting the Coordinator and losing the delegate callbacks
- ✗Not guarding the write-back, creating a state to delegate feedback loop
Follow-up questions
- →How do you stop a programmatic region change from retriggering the map delegate?
- →Who owns the
MKMapViewand the Coordinator, and when are they deallocated?
JuniorTheoryRareHow does the result builder @ViewBuilder turn views in body into a TupleView, and what is the 10-view limit?
How does the result builder @ViewBuilder turn views in body into a TupleView, and what is the 10-view limit?
@ViewBuilder is a result builder that combines the views in body into one composite value — for several children, a TupleView. There's no runtime loop; the compiler synthesizes it. Early SwiftUI had overloads only up to ten children.
Common mistakes
- ✗Thinking
@ViewBuilderbuilds a runtime array rather than a static composite - ✗Believing the ten-view limit is a style guideline, not a compiler constraint
- ✗Not knowing that several children become a
TupleView
Follow-up questions
- →Why does listing two views in
bodyproduce aTupleView? - →How does wrapping children in
Groupsidestep the ten-view limit?