Skip to content

feat(iOS, FormSheet v5): Add basic setup for standalone FormSheet native component#3947

Open
t0maboro wants to merge 81 commits intomainfrom
@t0maboro/formsheet-modal-v5
Open

feat(iOS, FormSheet v5): Add basic setup for standalone FormSheet native component#3947
t0maboro wants to merge 81 commits intomainfrom
@t0maboro/formsheet-modal-v5

Conversation

@t0maboro
Copy link
Copy Markdown
Contributor

@t0maboro t0maboro commented Apr 28, 2026

Description

Introducing the iOS implementation of the standalone FormSheet component.

Key Architectural Decisions:

  • On the JS side, the FormSheet is placed exactly where it is defined in the component tree, but its frame is intentionally cleared. It acts as a logical host component.
  • The actual UI is driven by a dedicated RNSFormSheetController. This controller creates a transparent top-level UIView and is presented modally over the current UIWindow hierarchy via the closest valid presentingViewController.
  • The sheet's visibility is entirely driven by the isOpen prop. This declarative boolean is translated into imperative presentViewController and dismissViewController calls. When a user natively dismisses the sheet (e.g., via a swipe-down gesture), the RNSFormSheetControllerDelegate triggers the RNSFormSheetComponentEventEmitter to fire the onDismiss event, allowing the JS to synchronize its local state.
  • To ensure React children are sized correctly within the dynamic sheet, we observe viewDidLayoutSubviews inside the presented controller. When the native bounds change, the new size and origin offset are captured and dispatched via a C++ state update. This explicitly forces Yoga to synchronously recalculate the content layout using the sheet's actual native dimensions.
  • Child components defined in JS are intercepted during Fabric's mount/unmount lifecycle. Instead of being added to the FormSheet's Host view, they are explicitly reparented into the presented controller's view.

Implementation details:

  • Support for customizable detents. Fully supports iOS 16+ custom detents, with a safe fallback to system detents for iOS 15. Detents MUST come in ascending order.
  • Implements an independent RCTSurfaceTouchHandler attached to the controller's view, utilizing viewOriginOffset to correctly align the React touch coordinate space with the natively presented window.
  • Delays the zeroing of the Shadow Node size until the dismissal animation completes to prevent the layout from collapsing while sliding down.

Changes

  • Added RNSFormSheetComponentView to handle the Fabric lifecycle, prop updates, and state management.
  • Added RNSFormSheetController to manage the native UIModalPresentationFormSheet presentation and subview hierarchy.
  • Added RNSFormSheetComponentEventEmitter to send onDismiss events.
  • Created RNSFormSheetShadowNode definitions for synchronous layout updates.
  • Added two Fabric test scenarios in the playground app to verify layout and stack v5 integration.

Before & after - visual documentation

Base layout test Stack v5 integration test
basic-layout.mov
formsheet-with-stack-v5.mov

Test plan

  1. FormSheet Test: Basic functionality
  • Verified the sheet opens at the initial detent when isOpen is set to true.
  • Verified that content layouts properly.
  • Expanded the sheet to the 1.0 detent and confirmed the layout dynamically adapts.
  • Tapped "Dismiss from JS" to verify programmatic dismissal safely triggers the transition and unmounts the view without layout collapse.
  1. FormSheet with Nested Stack v5
  • Opened the FormSheet and verified that the nested StackContainer fills the FormSheet's bounds
  • Pushed "Screen A" onto the nested stack and confirmed the navigation occurs entirely within the sheet.
  • Expanded the sheet to the maximum detent and verified the stack layout adapts.
  • Natively swiped down to dismiss the sheet, re-opened it via the JS button, and confirmed the nested stack state was preserved.

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants