ROOT CAUSE (confirmed by instrumentation):
1. viewportHeight was 0 — background GeometryReader onAppear fired
before ScrollView layout, never updated. Visibility ratio was
always 0.00, so wasVisible was never populated.
2. cumulativeDown was 0 — PreferenceKey + onPreferenceChange on the
zero-height anchor never delivered scroll offset updates.
3. Both tracking mechanisms were dead. Only per-row onChange fired.
FIX: Removed dead PreferenceKey scroll tracker and dead viewport
background GeometryReader. All tracking now lives in the per-row
GeometryReader onChange(of: frame.minY), which the logs confirmed
fires reliably:
- Scroll direction: computed from delta between current and previous
minY for each entry (stored in lastKnownMinY dictionary)
- Cumulative scroll: accumulated from positive deltas (>2pt filter)
- Activation: requires cumulative downward scroll > threshold
- Visibility: computed using UIScreen.main.bounds.height (reliable,
doesn't depend on layout timing)
- Mark condition: trackingActive + moving down + unread + was visible
+ maxY < 0 (fully above viewport)
Navigation protection preserved: onAppear resets trackingActive,
cumulativeDown, and lastKnownMinY.
Removed debug instrumentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>