Senior / Staff iOS — interview prep
UIKit & SwiftUI internals
lifecycle · layout · Auto Layout · lists · SwiftUI state · hosting · modals
Bounds timingFrame vs boundsHit testingConstraintsCell reuseForEach id@StateObjectHosting VCSheetsTimer vs DisplayLink
01
Two rendering models
big pictureUIKit
- Views have frames; you mutate hierarchy and properties
- Layout = constraints solved each pass → explicit frames
- You drive updates:
setNeedsLayout,layoutIfNeeded()
SwiftUI
- body describes a tree; runtime diffs and updates
- Identity (
id,ForEach) drives reuse—wrong id → lost row state - State wrappers tell the system what to store outside value-type views
i
Interview move: connect a symptom (wrong size, flicker, lost toggle) to the layer—UIKit geometry vs SwiftUI identity vs constraint ambiguity.
02
Bounds, layout & safe area
Q1When frames lie
viewDidLoad— wiring; bounds often not finalviewDidLayoutSubviews— aftersuper, geometry matches container + safe area for this passsafeAreaInsets— bars, keyboard, rotation change over time
Smells
- Sprinkling
layoutIfNeeded()to paper over bad constraints - Reading child frames from parent’s
viewDidLoad
Defer layer frames to layout
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
gradientLayer.frame = view.bounds
}
03
Frame, bounds & coordinate spaces
Q2frame
- Rectangle in the superview’s coordinate system
- origin + size — where the view sits in its parent
- Transform updates the axis-aligned bounding box in the parent (often larger after rotation)
bounds
- Rectangle in local space — drawing, hit testing, subview layout
- bounds.size — content extent; bounds.origin often
(0,0) UIScrollView:contentOffset↔ movingbounds.originto scroll
i
center is in the superview’s space; layout composes
center + bounds to derive frame. Mixing spaces without converting is the usual bug.Whose coordinates?
// frame → superview’s coordinate space (position + size)
// bounds → this view’s local space (drawing, subviews, scroll offset)
print(child.frame) // where child sits in parent
print(child.bounds) // child’s local width/height; origin often (0,0)
// After rotation: frame is axis-aligned bbox in parent;
// bounds.size often unchanged (untransformed content size).
04
Hit testing & focus
Q3Hit test
- Front → back among subviews; deepest hit that contains the point wins
- Needs
isUserInteractionEnabled, not hidden, reasonable alpha - Clear overlays can still steal touches
First responder
- Hit test ≠ keyboard focus
- SwiftUI:
@FocusState; UIKit:becomeFirstResponder
Keyboard — iOS 15+ layout guide
view.keyboardLayoutGuide.topAnchor.constraint(
greaterThanOrEqualTo: textField.bottomAnchor, constant: 8
).isActive = true
05
Auto Layout
Q4i
Engine solves linear constraints + intrinsic content size each pass. Unsatisfiable (impossible) ≠ ambiguous (under-constrained).
Content hugging
Resist growing beyond intrinsic size (label “hugs” its text).
Compression resistance
Resist shrinking below intrinsic (avoid truncating the wrong label).
Tie-break when two views compete
titleLabel.setContentHuggingPriority(.required, for: .vertical)
subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
06
Lists, reuse & identity
Q5 · Q7UIKit cells
- Cells are reused—reset images, tasks, selection in
prepareForReuse - After
asyncwork, re-check identity (item id) before assigning images UICollectionViewDiffableDataSource— snapshots, stable IDs, fewer batch-update bugs
SwiftUI ForEach
- Same real-world row → same
idevery time - Random
UUID()per body → new identity → lost row state
prepareForReuse — reset + cancel
override func prepareForReuse() {
super.prepareForReuse()
loadTask?.cancel()
loadTask = nil
boundItemId = nil
imageView.image = nil
}
Stable ForEach
ForEach(items) { item in
RowView(item: item) // Item: Identifiable, stable id from domain
}
07
SwiftUI state — who owns what
Q6| Wrapper | Use when | Avoid |
|---|---|---|
| @State | Local value type owned by this view | Storing reference types you meant to share |
| @Binding | Read/write through to parent state ($ in parent) | A second copy of the same model field |
| @StateObject | This view creates the ObservableObject (once) | Passing the object in from outside |
| @ObservedObject | Someone else created it; you observe | @ObservedObject var m = Model() in body — new instance every refresh |
| @EnvironmentObject | Injected above (environmentObject) — app/session scope | Missing injection → crash when read |
| @Observable / @Bindable | Macro observation (iOS 17+ style); bindings to observable props | Mixing duplicate stores for same domain entity |
Parent State → child Binding
struct Parent: View {
@State private var isOn = false
var body: some View { Child(isOn: $isOn) }
}
struct Child: View {
@Binding var isOn: Bool
}
08
SwiftUI performance (lists)
Q7Cheap rows
- No heavy decode / formatters in
body - Split subviews so parent changes don’t rebuild every row
Lazy vs eager
LazyVStackinScrollViewfor long content- Profile: Instruments SwiftUI / Time Profiler
09
Embedding SwiftUI in UIKit
Q8UIHostingController(rootView:)— child VC + pin edges; lifetime follows UIKit- Intrinsic content can be ambiguous—sometimes need a container with explicit constraints
UIHostingConfiguration(iOS 16+) for cells vs full hosting per row- Bridge UIKit → SwiftUI:
UIViewRepresentable/UIViewControllerRepresentable— coordinator for delegates, watch update loops
Child hosting controller
let host = UIHostingController(rootView: SettingsView())
addChild(host)
host.view.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(host.view)
// pin edges…
host.didMove(toParent: self)
10
Modals — data in & out
Q9Roles
- Presenter: whether it shows + what happens on end
- Presented: collects input; don’t silently mutate parent without rules
Patterns
- Callback / result on save vs cancel — one apply site
- Binding / shared store when editing one live source of truth
- weak delegates in UIKit; avoid apply-after-dismiss races
Explicit contract
// Presenting side owns isPresented + applies result
.sheet(isPresented: $showEditor) {
EditorView(
onSave: { value in model = value; showEditor = false },
onCancel: { showEditor = false }
)
}
11
Timer vs `CADisplayLink`
Q10Timer
- Fires on a wall-clock interval via the run loop—not locked to vsync
- Good for debounce, polling, “every N seconds”—coarse timing
- Add to
RunLoop.mainwith.commonif it must run duringUIScrollViewtracking
CADisplayLink
- Fires with the display refresh (~once per frame at current Hz)
- Use for frame-aligned custom stepping / drawing; read
durationfor delta - Invalidate when the view goes away—otherwise CPU keeps running
i
Prefer UIViewPropertyAnimator, Core Animation, or SwiftUI animations when they express the effect; reach for these primitives when you need explicit run-loop or frame pacing.
12
Quick checklist
Q1–Q10| Symptom | Look at |
|---|---|
| Wrong size at first paint | Bounds read too early; safe area; constraint ambiguity |
| Subview offset / touches misaligned | Mixed frame vs bounds spaces; transform + bounding box |
| Taps swallowed | Hit testing order; overlay alpha / isUserInteractionEnabled |
| Constraint warnings | Conflict vs ambiguity; intrinsic size; translatesAutoresizingMaskIntoConstraints |
| Wrong image in scrolling cell | Reuse + async: cancel, clear, verify id after await |
| Row state resets while scrolling | SwiftUI: unstable ForEach id |
| @Published never updates | Wrong wrapper: StateObject vs ObservedObject; new model in body |
| Sheet saves after Cancel | Commit semantics: callback vs shared model; async completion order |