UIKit Fundamentals
The view-controller lifecycle, `frame` vs `bounds`, cell reuse, the responder chain and hit-testing, `CALayer`, and gesture recognizers.
12 questions
JuniorTheoryVery commonWhat does dequeueReusableCell do, and what bug appears if you skip prepareForReuse?
What does dequeueReusableCell do, and what bug appears if you skip prepareForReuse?
dequeueReusableCell returns a recycled cell from a pool rather than allocating a new one, so scrolling stays cheap. The cell still holds the previous row's state, so skipping prepareForReuse leaves stale text or images on the wrong row.
Common mistakes
- ✗Assuming a dequeued cell arrives cleared instead of holding old state
- ✗Thinking
dequeueReusableCellallocates a new cell on every call - ✗Believing
prepareForReuseis optional cosmetic bookkeeping
Follow-up questions
- →Where else besides
prepareForReusecan you reset a recycled cell? - →Why can an in-flight async image load still corrupt a reused cell?
JuniorTheoryVery commonIn what order do viewDidLoad, viewWillAppear, and viewDidAppear run, and what belongs in each?
In what order do viewDidLoad, viewWillAppear, and viewDidAppear run, and what belongs in each?
viewDidLoad runs once after the view loads — put one-time setup here. viewWillAppear runs before every appearance — refresh data and UI. viewDidAppear runs after the view is on screen — start animations.
Common mistakes
- ✗Putting data-refresh in
viewDidLoad, so it never updates on return - ✗Assuming
viewWillAppearruns only once instead of on every appearance - ✗Starting animations in
viewWillAppearbefore the view is on screen
Follow-up questions
- →Why can
viewDidLoadrun before the view has its final size? - →When does
viewWillAppearfire again after a push-then-pop?
JuniorCodeCommonSet up a UITableViewDiffableDataSource with a snapshot
Set up a UITableViewDiffableDataSource with a snapshot
Create the data source with a cell-provider closure per Item. Make an NSDiffableDataSourceSnapshot, appendSections([.main]), appendItems(items), then dataSource.apply(snapshot). Unlike reloadData, it animates only the changed rows.
Common mistakes
- ✗Calling
reloadDatainstead of applying a snapshot - ✗Forgetting
appendSectionsbeforeappendItems - ✗Thinking diffable data sources are collection-view only
Follow-up questions
- →How does
apply(_:animatingDifferences:)decide the row animations? - →Why must
ItembeHashablefor the diff to work?
JuniorTheoryCommonWhat is the difference between a view's frame and its bounds, and when is bounds.origin not (0,0)?
What is the difference between a view's frame and its bounds, and when is bounds.origin not (0,0)?
frame is the view's rect in its superview's coordinates — position and size. bounds is the same rect in the view's own coordinates, so its origin is usually (0,0). A scroll view shifts bounds.origin to scroll, which is when it differs.
Common mistakes
- ✗Believing
frameandboundsare always the same rectangle - ✗Thinking
bounds.origincan never be anything but(0,0) - ✗Confusing which rectangle is expressed in the superview's coordinates
Follow-up questions
- →How does applying a
CGAffineTransformaffectframeversusbounds? - →Why do you read
bounds, notframe, insidelayoutSubviews?
JuniorTheoryCommonWhat are a UIGestureRecognizer's states, why do two recognizers conflict, and what does require(toFail:) do?
What are a UIGestureRecognizer's states, why do two recognizers conflict, and what does require(toFail:) do?
A recognizer moves through possible, active states for continuous gestures or recognized for discrete, plus failed/cancelled. Two conflict when one touch suits both, like a tap in a pan. require(toFail:) makes one wait for another to fail.
Common mistakes
- ✗Thinking recognizers have no
failed/cancelledstates - ✗Believing
require(toFail:)disables the other recognizer - ✗Assuming two recognizers can never receive the same touch
Follow-up questions
- →How does
shouldRecognizeSimultaneouslyWithdiffer fromrequire(toFail:)? - →Why does a discrete tap skip the
began/changedstates?
JuniorTheoryCommonWhat is the responder chain, how does an action reach a handler, and what is the first responder?
What is the responder chain, how does an action reach a handler, and what is the first responder?
The responder chain is an ordered list of UIResponder objects — view, superviews, controller, window, app. A nil-target action travels up it until one implements the selector. The first responder is the head, usually the focused control.
Common mistakes
- ✗Thinking actions broadcast to all views rather than walking a chain
- ✗Believing the chain runs top-down from the app to the leaf view
- ✗Confusing the first responder with the app delegate
Follow-up questions
- →How does
becomeFirstResponderchange where keyboard input goes? - →Where does the chain continue past a view controller's own view?
JuniorTheoryCommonWhat do you gain and lose with Storyboards, XIBs, and code-only UI, especially on a large team?
What do you gain and lose with Storyboards, XIBs, and code-only UI, especially on a large team?
Storyboards show flow visually and start fast but merge poorly, with git conflicts on a big team. XIBs are smaller per-view files that conflict less and reuse well. Code-only UI avoids merge pain and is most reviewable, but gives no visual preview.
Common mistakes
- ✗Calling Storyboards the most merge-friendly option
- ✗Thinking code-only UI is harder to diff than a Storyboard
- ✗Assuming the three choices have no team-scale consequences
Follow-up questions
- →How do container-view segues reduce a Storyboard's merge surface?
- →Why is a code-only layout easier to unit-test than a XIB?
JuniorTheoryCommonWhat do UIView and CALayer each own, and what does 'layer-backed view' mean?
What do UIView and CALayer each own, and what does 'layer-backed view' mean?
Every UIView is backed by a CALayer that does the drawing, geometry, and animation; the view adds touch handling and the responder chain. 'Layer-backed' means rendering is delegated to that layer, so cornerRadius lives on the layer.
Common mistakes
- ✗Thinking a view draws itself rather than delegating to its layer
- ✗Believing views and layers are unrelated objects wired by hand
- ✗Placing
cornerRadius/shadowon the view expecting the layer's behavior
Follow-up questions
- →Why can you animate a layer property that has no
UIViewequivalent? - →What is the difference between a view's frame and its layer's frame?
MiddleDebuggingCommonFix a table where fast-scrolled rows briefly show another row's image
Fix a table where fast-scrolled rows briefly show another row's image
The completion captures the reused cell, recycled to another row before the image arrives, so it paints the wrong avatar. Reset the image in prepareForReuse and, on completion, confirm the cell still maps to that URL, else drop the stale load.
Common mistakes
- ✗Blaming
IndexPathinstability instead of cell recycling - ✗Fixing it by disabling reuse instead of guarding the completion
- ✗Forgetting to reset the image before the async load starts
Follow-up questions
- →How does a per-cell load token let you ignore an outdated completion?
- →Why does resetting in
prepareForReusestill leave a flash without cancellation?
MiddleTheoryCommonWhat do setNeedsLayout, layoutIfNeeded, layoutSubviews, and setNeedsDisplay do, and when does each run?
What do setNeedsLayout, layoutIfNeeded, layoutSubviews, and setNeedsDisplay do, and when does each run?
setNeedsLayout marks the view dirty so layoutSubviews runs on the next pass; layoutIfNeeded forces that pass now. layoutSubviews positions subviews and is never called directly. setNeedsDisplay schedules a redraw via draw(_:), a separate pass.
Common mistakes
- ✗Calling
layoutSubviewsdirectly instead ofsetNeedsLayout - ✗Thinking
setNeedsLayoutlays out synchronously likelayoutIfNeeded - ✗Confusing the redraw pass (
setNeedsDisplay) with the layout pass
Follow-up questions
- →Why batch several
setNeedsLayoutcalls before onelayoutIfNeeded? - →When does the run loop actually flush a pending layout pass?
MiddleTheoryOccasionalHow do hitTest and point(inside:) pick the touched view, and how do you make a button tappable beyond its parent's bounds?
How do hitTest and point(inside:) pick the touched view, and how do you make a button tappable beyond its parent's bounds?
hitTest walks the view tree back-to-front, calling point(inside:); the deepest view whose bounds holds the point wins. A touch outside the parent's bounds is discarded, so to enlarge a tap area override point(inside:) to accept outside points.
Common mistakes
- ✗Thinking
clipsToBounds = falsealone extends the tappable area - ✗Believing
hitTestusesframe/z-order instead ofboundsgeometry - ✗Assuming a touch outside a parent's
boundsstill reaches its child
Follow-up questions
- →Why does a child touch get clipped even when the child draws past the parent?
- →How does returning
nilfromhitTestmake a view transparent to touches?
MiddleDesignOccasionalYou are building a reusable pager component that hosts several full child screens — each a self-contained UIViewController with its own data loading, network calls, and lifecycle needs. You could stuff every screen's views into one giant controller, or make each screen a real child view controller inside a custom container. Explain when a custom container view controller is the right choice over a single monolithic controller, which containment calls (addChild, didMove(toParent:), willMove(toParent:), removeFromParent) you must make and in what order when adding and removing a child, and what breaks — appearance callbacks, traitCollection propagation, rotation, memory — if you skip them and just add the child's view as a subview.
You are building a reusable pager component that hosts several full child screens — each a self-contained UIViewController with its own data loading, network calls, and lifecycle needs. You could stuff every screen's views into one giant controller, or make each screen a real child view controller inside a custom container. Explain when a custom container view controller is the right choice over a single monolithic controller, which containment calls (addChild, didMove(toParent:), willMove(toParent:), removeFromParent) you must make and in what order when adding and removing a child, and what breaks — appearance callbacks, traitCollection propagation, rotation, memory — if you skip them and just add the child's view as a subview.
Use a container when each screen is a self-contained controller with its own lifecycle, staying decoupled rather than one monolith. Adding a child is addChild, insert child.view, then didMove(toParent:); removing is willMove(toParent: nil), remove the view, removeFromParent. Skip them and appearance and rotation stop forwarding.
Common mistakes
- ✗Adding
child.viewas a subview withoutaddChild/didMove - ✗Getting the add/remove call order wrong so callbacks misfire
- ✗Assuming appearance and trait events forward without containment
Follow-up questions
- →Why must
didMove(toParent:)come after inserting the child's view? - →How does
shouldAutomaticallyForwardAppearanceMethodschange this flow?