feat(iOS, FormSheet v5): Add support for fitToContents#4095
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new iOS-only fitToContents detent mode to the Gamma FormSheet so the sheet can size itself to its React content and animate as the content height changes.
Changes:
- Extends the JS API to accept
detents="fitToContents"and maps it to a native sentinel detent value. - Introduces a new Fabric native component (
RNSFormSheetContentWrapper) that reports Yoga-driven height changes to the FormSheet host. - Updates iOS detent resolution to support a “fit to contents” custom detent, and adds a dedicated single-feature-test scenario.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/fabric/gamma/modals/form-sheet/FormSheetContentWrapperNativeComponent.ts | Adds a new codegen native component for the content wrapper view. |
| src/components/gamma/modals/form-sheet/FormSheetUtils.ts | Adds JS-side mapping from 'fitToContents' to a native sentinel detent value. |
| src/components/gamma/modals/form-sheet/FormSheet.types.ts | Extends the public detents prop type/docs to include 'fitToContents'. |
| src/components/gamma/modals/form-sheet/FormSheet.tsx | Wraps children in the native content wrapper and passes resolved detents to native. |
| package.json | Registers the new iOS Fabric component class in codegenConfig. |
| ios/stubs/RNSGammaStubs.mm | Adds a stub implementation for the new Gamma component view. |
| ios/stubs/RNSGammaStubs.h | Adds a stub declaration for the new Gamma component view. |
| ios/gamma/modals/form-sheet/RNSFormSheetProviders.h | Extends the behavior provider interface with contentHeight. |
| ios/gamma/modals/form-sheet/RNSFormSheetHostComponentView.mm | Implements the content-height delegate and triggers behavior updates. |
| ios/gamma/modals/form-sheet/RNSFormSheetDetentResolver.mm | Adds native handling for the fit-to-contents detent via a custom detent resolver. |
| ios/gamma/modals/form-sheet/RNSFormSheetDetentResolver.h | Adds the fit-to-contents sentinel constant and updates the resolver API. |
| ios/gamma/modals/form-sheet/RNSFormSheetContentWrapperDelegate.h | Introduces a delegate protocol for content-height updates. |
| ios/gamma/modals/form-sheet/RNSFormSheetContentWrapperComponentView.mm | New component view that reports initial + dynamic height changes. |
| ios/gamma/modals/form-sheet/RNSFormSheetContentWrapperComponentView.h | Declares the new component view. |
| ios/gamma/modals/form-sheet/RNSFormSheetContentView.h | Adds a contentWrapperDelegate property to route height updates. |
| ios/gamma/modals/form-sheet/RNSFormSheetConfigurationApplicator.mm | Switches to the updated detent resolver API. |
| apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-fit-to-contents-ios/scenario.md | Adds a manual SFT scenario for fitToContents. |
| apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-fit-to-contents-ios/scenario-description.ts | Registers scenario metadata for the new SFT. |
| apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-fit-to-contents-ios/index.tsx | Adds a runnable UI scenario to expand/collapse content inside the sheet. |
| apps/src/tests/single-feature-tests/form-sheet/index.ts | Adds the new scenario to the form-sheet SFT group. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| @interface RNSFormSheetContentWrapperComponentView : RNSReactBaseView |
There was a problem hiding this comment.
Note for reviewers: I'm considering renaming it to RNSFormSheetReactContentWrapperComponentView, because I already have RNSFormSheetContentView, which overrides native UIView, and I want to have that one as a top-level view for potential native testing. As for now, these two names are too close for me, but in the same time, the name above I'm proposing for a native component is too long.
d130afe to
53ecbf8
Compare
e3558da to
6121c7d
Compare
LKuchno
left a comment
There was a problem hiding this comment.
Looks good :)
I marked few places to apply small changes in scenario - mostly after changing e2e section naming convention.
|
|
||
| ## E2E test | ||
|
|
||
| Other: Planned, but will be implemented separately. |
There was a problem hiding this comment.
Following new naming (described in RFC), if we are planning to implement e2e test I would change this line to:
| Other: Planned, but will be implemented separately. | |
| TBD: Planned, but will be implemented separately. |
|
|
||
| 2. Tap the "Open FormSheet" button. | ||
|
|
||
| - [ ] Expected: The FormSheet opens smoothly. Its height is matched to its internal content (it will have an extra empty space on the bottom which is originating from native inset application). There are no visual jumps during the initial presentation animation. |
There was a problem hiding this comment.
This part is applicable only for iPhone:(it will have an extra empty space on the bottom which is originating from native inset application) ? If yes, can you add note about this?
There was a problem hiding this comment.
For iPad while collapsing content inside form sheet empty space on the bottom appear:
Screen.Recording.2026-05-27.at.14.24.17.mov
| import type { FormSheetProps } from './FormSheet.types'; | ||
|
|
||
| // Predefined value for `fitToContents`. Keep in sync with native counterpart. | ||
| export const NATIVE_FIT_TO_CONTENTS = -1.0; |
There was a problem hiding this comment.
- Not sure if we need to export this?
- Other consts start with
FORM_SHEETprefix - maybe this one should as well?
| * @summary Defines the resting positions of the sheet. | ||
| * | ||
| * This can be either an array of fractional screen heights (ranging from `0.0` to `1.0`) | ||
| * or the `'fitToContents'` string literal. |
There was a problem hiding this comment.
According to current convention we use fitToContents directly without '. Same below.
| * This can be either an array of fractional screen heights (ranging from `0.0` to `1.0`) | ||
| * or the `'fitToContents'` string literal. | ||
| * | ||
| * - **Fractional heights:** The sheet will snap to these specific height proportions. |
There was a problem hiding this comment.
do we need the bold here?
| @@ -8,6 +9,9 @@ | |||
| static BOOL RNSAreDetentsValid(const std::vector<double> &detents) | |||
| - (id<RNSFormSheetContentWrapperDelegate>)resolveFormSheetContentWrapperDelegate | ||
| { | ||
| UIView *view = self.superview; | ||
| while (view != nil) { | ||
| if ([view isKindOfClass:[RNSFormSheetContentView class]]) { | ||
| return ((RNSFormSheetContentView *)view).contentWrapperDelegate; | ||
| } | ||
| view = view.superview; | ||
| } | ||
| return nil; | ||
| } |
There was a problem hiding this comment.
I'm not sure if I'm the biggest fan of searching for RNSFormSheetContentView and extracting contentWrapperDelegate from there. Should RNSFormSheetContentView be even aware of any content wrapping logic? In native impl world we have behaviorProvider which returns contentHeight -> it seems to me that this should be enough for native implementation. How behaviorProvider (react) calculates this height should be a separate concern that is not related to RNSFormSheetContentView.
Did you consider doing something like this:
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_controller.contentView insertReactSubview:childComponentView atIndex:index];
if ([childComponentView isKindOfClass:[RNSFormSheetContentWrapperComponentView]]) {
((RNSFormSheetContentWrapperComponentView*)childComponentView).delegate = self;
}
}Obviously with weak ref and proper cleanup in unmountChildComponentView.
There was a problem hiding this comment.
Then we should also be able to avoid awkward mountingTransactionDidMount.
Description
This PR introduces support for the
fitToContentsdetents setup in iOS FormSheet stanadlone component.It automatically calculates its initial height based on its React children and animate whenever the size of the content changes dynamically.
Closes: https://github.com/software-mansion/react-native-screens-labs/issues/1266
Implementation Details
For a predictable codegen interface, I'm exposing in a public type 'fitToContents' literal, which under the hood is mapped to
[-1.0]array which is handled natively as a special case.I introduced a transparent from end-user's perspective native component responsible for listening to Yoga's layout calculations via
updateLayoutMetrics.For ScreenContentWrapper on the JS side, I conditionally applied
bottom: 0style. This ensures that standard system detents stretch the content to fill the available space, while thefitToContentsmode correctly wraps its children.Natively, the wrapper traverses up the superview chain to find the native ancestor
RNSFormSheetContentViewand dynamically resolves its delegateRCTMountingTransactionObservingwas implemented for ContentWrapper to proactively report the initial layout height as soon as the native view hierarchy is linked.Before & after - visual documentation
ios18.mov
ios26.mov
ipad.mov
Test plan
Added dedicated SFT, went through some random examples to ensure no regression.
Checklist