Summary
This postmortem analyzes a subtle but real-world UIKit rendering issue: a UICollectionViewCell that displays correctly on one simulator (iPhone 16) but fails to show its glow highlight on another (iPhone 16 Pro Max) until the user interacts with the carousel. The behavior stems from layout timing, shadow-path initialization, and scroll-position–dependent selection logic.
Root Cause
The underlying issue is a combination of:
- Shadow rendering depending on
layoutSubviews()timing, which differs across device sizes. - Selection logic tied to distance calculations (
distance < 1), which evaluates incorrectly before the first layout pass on larger screens. - Glow view shadow properties not being applied until the cell is fully laid out, causing the highlight to appear only after scrolling triggers a layout update.
didMoveToSuperview()not guaranteeing correct geometry, especially on large devices where Auto Layout resolves later.
The result: the initial selected cell appears unselected because the glow shadow is not yet valid.
Why This Happens in Real Systems
This class of bug is extremely common in UIKit because:
- Shadow paths require final bounds, but bounds are not final during initialization.
- Collection views perform multiple layout passes, and the first pass may not include the correct content offset.
- Large-screen devices trigger different timing for layout and scrolling, exposing race conditions.
- Selection logic tied to scroll position is fragile, especially in infinite carousels.
Real-World Impact
These issues can cause:
- Incorrect initial UI state, confusing users.
- Inconsistent behavior across devices, making QA difficult.
- Hard-to-reproduce visual bugs, especially in carousels and paged layouts.
- Performance regressions if developers try to “fix” it by forcing layout repeatedly.
Example or Code (if necessary and relevant)
Below is a minimal example of how senior engineers stabilize glow rendering by ensuring the shadow path is set after layout is final:
override func layoutSubviews() {
super.layoutSubviews()
glowView.layer.cornerRadius = glowView.bounds.width / 2
glowView.layer.shadowPath = UIBezierPath(ovalIn: glowView.bounds).cgPath
}
And ensuring selection is applied after the collection view finishes its initial layout:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !didSetInitialSelection {
didSetInitialSelection = true
applySelectionToVisibleCells()
}
}
How Senior Engineers Fix It
Experienced engineers typically apply a combination of these strategies:
- Move all geometry-dependent logic (corner radius, shadow path) into
layoutSubviews(), notdidMoveToSuperview(). - Defer initial selection until after the first layout pass, using:
viewDidLayoutSubviews()collectionView.layoutIfNeeded()DispatchQueue.main.async { … }as a last resort
- Avoid selection logic tied to scroll distance until the collection view has a stable content offset.
- Reapply selection state in
cellForItemAt, ensuring cells always reflect the model state. - Use
preferredLayoutAttributesFittingwhen necessary to guarantee correct sizing.
Why Juniors Miss It
Less experienced developers often overlook these patterns because:
- UIKit’s layout timing is non-intuitive, especially with nested views and shadows.
- Simulators hide timing differences, so a bug appears only on certain devices.
- They assume
didMoveToSuperview()means “layout is ready”, which is incorrect. - They rely on scroll-based selection logic without understanding when offsets become valid.
- They don’t realize shadow paths require final bounds, not initial ones.
The result is a bug that looks random but is actually deterministic once you understand UIKit’s layout lifecycle.