Skip to content

[iOS][Fabric] ScrollView content permanently invisible: inner RCTEnhancedScrollView left at CGRectZero when the autoresizing delta is missed #57520

Description

@davidecallegaro

Description

On the New Architecture, RCTScrollViewComponentView sizes its inner RCTEnhancedScrollView only via an autoresizing mask: the scroll view is created with initWithFrame:self.bounds — which is CGRectZero at component-view creation — and relies on UIViewAutoresizingFlexibleWidth | FlexibleHeight deltas to grow when the shell's frame is later applied (source).

Autoresizing only propagates deltas at the moment the parent's frame changes. Under heavy mount/teardown churn (reliably reproducible on a thermally-throttled device, where JS-driven mount choreography stretches from milliseconds to seconds), that delta moment can be missed. The inner UIScrollView is then left at CGRectZero permanently — there is no code path that ever re-establishes the invariant:

  • the shell has its correct Yoga frame (onLayout fires normally),
  • onContentSizeChange fires normally (state events come from the ShadowTree, not the native view),
  • the content subtree is fully mounted and correctly laid out inside the scroll view,
  • but everything is clipped into a zero-size viewport → content is invisible while the app believes everything is fine.

Captured native hierarchy of a live occurrence (Xcode view debugger, real device):

RCTScrollViewComponentView   frame = (0, 429, 430, 503)   ← correct Yoga frame
  RCTEnhancedScrollView      frame = (0, 0, 0, 0)         ← never sized
    UIView (container)       frame = (0, 0, 430, 1024)    ← matches contentSize
      … full content tree, all frames correct, invisible …

To identify the writer we swizzled -[RCTEnhancedScrollView setFrame:] to veto+stack-trace any zero-size write: it never fires. Nothing writes a zero frame — the view is born at zero and the grow-delta is missed. This distinguishes the bug from external interference and points at the reliance on autoresizing itself.

Frequency in the wild: on a hot iPhone 15 Pro Max (thermal state "serious") this hit nearly every mount of screens whose ScrollView mounts one frame after the screen (skeleton→content swap), across multiple independent screens. It is also consistent with long-standing "ScrollView renders invisible after logout/onboarding transition" reports that apps (including ours) have historically dodged with full app reloads.

Related (but distinct, both recycling-related — recycling is off by default and was disabled in our repro): #55090, #34165.

Steps to reproduce

The failure is a race, so there is no minimal synchronous repro; the deterministic statement of the bug is:

  1. RCTScrollViewComponentView is created (frame CGRectZero) → _scrollView created at zero with autoresizing W+H.
  2. If the shell's frame assignment lands in any state where autoresizing cannot propagate the delta to _scrollView, the scroll view stays 0×0.
  3. No subsequent code (updateLayoutMetrics, state updates, layoutSubviews) ever corrects it → permanent invisible content.

Practical reproduction (how we hit it ~every mount): iPhone at thermal state serious (charge + load), a screen that mounts its ScrollView one requestAnimationFrame after the screen mounts, navigate to the screen repeatedly. Verified on RN 0.83.6 / iOS 26.5 / New Architecture / Hermes; the relevant code is unchanged on main.

Expected / actual

  • Expected: the inner scroll view always tracks the shell's bounds.
  • Actual: it can be left at CGRectZero forever; all content invisible; no error anywhere (JS, ShadowTree, or UIKit).

Fix

PR attached: enforce the invariant explicitly — re-glue _scrollView to the shell's bounds in updateLayoutMetrics and layoutSubviews (bounds.size + center rather than frame, so the RTL transform is respected and bounds.origin = contentOffset is preserved). Verified on-device: the desync still occurs under the same conditions and now heals within one layout pass; the invisible-content states no longer reproduce.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs: Author FeedbackNeeds: ReproThis issue could be improved with a clear list of steps to reproduce the issue.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions