SwiftUI cheat sheet
The 22 SwiftUI interview answers in 30 seconds each, plus quick facts. Printable, free.
One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions
SwiftUI vs UIKit: key differences
SwiftUI is declarative: you describe the UI for the current state, and the framework re-evaluates body and updates only what changed. UIKit is imperative: you create views and mutate them yourself. SwiftUI views are cheap value types; UIKit views are long-lived objects. Most apps mix both through representables and hosting controllers.
Which property wrapper for which data?
Ask who owns the data. The view that creates it owns it with @State (values or an @Observable model). A child that must write a parent's value takes @Binding. A child that needs bindings into an @Observable model uses @Bindable. Shared app-wide models go in .environment(model) and are read with @Environment(Model.self).
@State vs a plain var
A view is an immutable struct that SwiftUI recreates often, so body can't mutate its own stored properties. @State moves the value into storage that SwiftUI owns and keeps for the lifetime of the view's identity. Changing it invalidates the view, and SwiftUI re-runs body.
@StateObject vs @ObservedObject
Both observe an ObservableObject, but @StateObject creates and owns it, keeping one instance for the view's lifetime. @ObservedObject just watches an object someone else owns. If a view creates its model with @ObservedObject, every parent re-render makes a new instance and the state resets. On iOS 17+, use @State with @Observable instead.
@Observable model keeps resetting
The child view is recreated whenever its parent's body runs, so a model stored in a plain property is recreated too. The view that creates an @Observable model must own it with @State. Children receive it as a plain property, or with @Bindable when they need bindings.
@Observable vs ObservableObject
@Observable is a macro that tracks which properties each view's body actually reads, and updates only those views when one of them changes. ObservableObject publishes one objectWillChange for any @Published change, so every observing view updates. Observation also needs no wrappers for reading: a plain property is enough.
@Bindable for passed-in models
@Bindable lets a view create bindings ($model.name) to properties of an @Observable model it was given but doesn't own. Use it when a child edits a model with TextField, Toggle or Slider. It doesn't own or keep the object alive; the owner still holds it in @State.
@Binding and the $ prefix
@Binding is a two-way reference to a value owned somewhere else, usually a parent's @State. The $ prefix reads a property wrapper's projected value. On @State and @Bindable it gives a Binding; on an @Published property inside an ObservableObject it gives a Combine publisher, Published<Value>.Publisher.
View identity: if/else vs a modifier
SwiftUI keeps state per view identity. Identity comes from the view's position in the hierarchy (structural identity) or an explicit .id() or ForEach id. An if/else makes two different branches, so switching branches destroys one view, with its state, and creates the other. Prefer changing a modifier's value when it is really the same view.
Why body runs often, and keeping it cheap
A View is a lightweight description, not the rendered view. SwiftUI calls body whenever something it depends on changes, then diffs the result. Calling body often is normal and cheap if body is cheap. It becomes a problem when body does real work (formatting, sorting, network calls) or depends on state it doesn't need.
.task vs .onAppear + Task
.task starts async work when the view appears and cancels it automatically when the view disappears. .task(id:) also cancels and restarts it when the id changes. A Task {} started in .onAppear is unstructured: nobody cancels it, it may finish after the view is gone, and it doesn't re-run when inputs change.
.task(id:) and built-in debouncing
Each time the id changes, SwiftUI cancels the running task and starts a new one. If the task sleeps briefly before doing work, only the last change survives, which gives a debounce with no Combine. The pattern is: sleep, check cancellation, then load.
NavigationStack and deep links
NavigationStack is driven by a path: an array of Hashable route values (or a NavigationPath). .navigationDestination(for:) maps each route type to a screen. To deep link, parse the URL into routes and set the path. Because the path is data, you can save it, restore it and test it.
Custom environment values with @Entry
Environment values flow down the view tree. Since Xcode 16 you declare one with @Entry inside extension EnvironmentValues, which generates the key and accessors for you. Set it with .environment(\.accent, .red) and read it with @Environment(\.accent). Before @Entry you wrote an EnvironmentKey type by hand.
ViewModifier vs AnyView
A ViewModifier packages reusable styling or behaviour, applied with .modifier() or a View extension. It keeps concrete types, so SwiftUI can diff efficiently. AnyView erases the type, which hides structure from SwiftUI, can hurt performance, and makes identity harder to reason about.
.animation(_:value:) vs withAnimation
SwiftUI animates the difference between two states. withAnimation { } animates every change caused by that state update. .animation(.spring, value: x) animates changes inside the view it is attached to, but only when x changes. Transitions animate views being inserted or removed.
GeometryReader vs ViewThatFits and Layout
GeometryReader gives you the proposed size, but it takes all available space and pushes layout into manual math. For 'pick whichever fits', use ViewThatFits (iOS 16). To switch between HStack and VStack while keeping state, use AnyLayout. For custom arrangements, write a Layout. Use GeometryReader mainly for reading sizes, often in a background.
UIViewRepresentable feedback loops
Wrap the UIKit view in a UIViewRepresentable: create it once in makeUIView, push SwiftUI state into it in updateUIView, and receive UIKit events through a Coordinator. updateUIView runs often, so it must be cheap and must only apply real changes; otherwise a delegate callback that writes back to a binding can cause an update loop.
Loading and error state in SwiftUI
Keep loading state in a @MainActor model and make it impossible to get stuck: reset the spinner with defer, or use one enum (idle, loading, loaded, failed). Show errors with the modern .alert(_:isPresented:presenting:actions:message:) API, offer Retry, and ignore CancellationError.
.alert("Couldn't load", isPresented: $showError, presenting: model.error) { _ in
Button("Retry") { Task { await model.load() } }
Button("Cancel", role: .cancel) { }
} message: { error in
Text(error.localizedDescription)
}@FocusState
@FocusState tracks which control has keyboard focus. Bind it to fields with .focused($focus, equals: .email), set it to move focus, and set it to nil to dismiss the keyboard. An optional enum covers forms with many fields.
@ViewBuilder
@ViewBuilder is a result builder that turns several view expressions, if/else and switch into one view type. body already uses it. Add it to your own functions, computed properties or closure parameters when they need to return different views in different branches, instead of erasing them with AnyView.
A timer that re-renders the whole screen
Find what the timer invalidates. Usually the ticking state lives high up, so every tick re-runs a big body. Move the time display into its own small view (or a TimelineView), so only that view depends on the clock. Confirm with Self._printChanges() and the SwiftUI instrument before and after.
Quick facts
- .sheet vs .fullScreenCover
- A sheet is a card the user can swipe down on iPhone; fullScreenCover covers the screen and must be dismissed in code.
- AnyLayout
- Wraps a Layout (for example HStackLayout or VStackLayout) so you can switch layouts without losing child state.
- @Previewable
- Lets you declare @State directly inside a #Preview block (Xcode 16).
- Self._printChanges()
- Debug-only helper that prints which dependency made a view's body run.
- .id(_:)
- Gives a view an explicit identity; changing the value resets its state and triggers transitions.
- PreferenceKey
- Sends values from children up to ancestors, read with onPreferenceChange.
- @ScaledMetric
- Scales a number (padding, icon size) with the user's Dynamic Type setting.