← All topics/UIKit & SwiftUI internals

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 picture
UIKit
  • 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

Q1
When frames lie
  • viewDidLoad — wiring; bounds often not final
  • viewDidLayoutSubviews — after super, geometry matches container + safe area for this pass
  • safeAreaInsets — 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

Q2
frame
  • 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 ↔ moving bounds.origin to 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

Q3
Hit 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 testkeyboard focus
  • SwiftUI: @FocusState; UIKit: becomeFirstResponder
Keyboard — iOS 15+ layout guide
view.keyboardLayoutGuide.topAnchor.constraint(
    greaterThanOrEqualTo: textField.bottomAnchor, constant: 8
).isActive = true
05

Auto Layout

Q4
i
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 · Q7
UIKit cells
  • Cells are reused—reset images, tasks, selection in prepareForReuse
  • After async work, re-check identity (item id) before assigning images
  • UICollectionViewDiffableDataSource — snapshots, stable IDs, fewer batch-update bugs
SwiftUI ForEach
  • Same real-world row → same id every 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
WrapperUse whenAvoid
@StateLocal value type owned by this viewStoring reference types you meant to share
@BindingRead/write through to parent state ($ in parent)A second copy of the same model field
@StateObjectThis view creates the ObservableObject (once)Passing the object in from outside
@ObservedObjectSomeone else created it; you observe@ObservedObject var m = Model() in body — new instance every refresh
@EnvironmentObjectInjected above (environmentObject) — app/session scopeMissing injection → crash when read
@Observable / @BindableMacro observation (iOS 17+ style); bindings to observable propsMixing 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)

Q7
Cheap rows
  • No heavy decode / formatters in body
  • Split subviews so parent changes don’t rebuild every row
Lazy vs eager
  • LazyVStack in ScrollView for long content
  • Profile: Instruments SwiftUI / Time Profiler
09

Embedding SwiftUI in UIKit

Q8
  • UIHostingController(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

Q9
Roles
  • 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`

Q10
Timer
  • 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.main with .common if it must run during UIScrollView tracking
CADisplayLink
  • Fires with the display refresh (~once per frame at current Hz)
  • Use for frame-aligned custom stepping / drawing; read duration for 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
SymptomLook at
Wrong size at first paintBounds read too early; safe area; constraint ambiguity
Subview offset / touches misalignedMixed frame vs bounds spaces; transform + bounding box
Taps swallowedHit testing order; overlay alpha / isUserInteractionEnabled
Constraint warningsConflict vs ambiguity; intrinsic size; translatesAutoresizingMaskIntoConstraints
Wrong image in scrolling cellReuse + async: cancel, clear, verify id after await
Row state resets while scrollingSwiftUI: unstable ForEach id
@Published never updatesWrong wrapper: StateObject vs ObservedObject; new model in body
Sheet saves after CancelCommit semantics: callback vs shared model; async completion order
UIKit & SwiftUI internals — interview prep cheat sheetAligned with practical question set