Skip to content

feat(iOS, FormSheet v5): Add support for fitToContents#4095

Open
t0maboro wants to merge 15 commits into
mainfrom
@t0maboro/fit-to-contents
Open

feat(iOS, FormSheet v5): Add support for fitToContents#4095
t0maboro wants to merge 15 commits into
mainfrom
@t0maboro/fit-to-contents

Conversation

@t0maboro
Copy link
Copy Markdown
Contributor

@t0maboro t0maboro commented May 25, 2026

Description

This PR introduces support for the fitToContents detents 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

  1. 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.

  2. I introduced a transparent from end-user's perspective native component responsible for listening to Yoga's layout calculations via updateLayoutMetrics.

  3. For ScreenContentWrapper on the JS side, I conditionally applied bottom: 0 style. This ensures that standard system detents stretch the content to fill the available space, while the fitToContents mode correctly wraps its children.

  4. Natively, the wrapper traverses up the superview chain to find the native ancestor RNSFormSheetContentView and dynamically resolves its delegate

  5. RCTMountingTransactionObserving was implemented for ContentWrapper to proactively report the initial layout height as soon as the native view hierarchy is linked.

Before & after - visual documentation

iOS 18 iOS 26 iPadOS
ios18.mov
ios26.mov
ipad.mov

Test plan

Added dedicated SFT, went through some random examples to ensure no regression.

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

@t0maboro t0maboro changed the base branch from main to @t0maboro/formsheet-presentation-state May 25, 2026 11:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread ios/gamma/modals/form-sheet/RNSFormSheetDetentResolver.mm
Comment thread ios/gamma/modals/form-sheet/RNSFormSheetContentWrapperDelegate.h
Comment thread ios/gamma/modals/form-sheet/RNSFormSheetContentWrapperComponentView.h Outdated

NS_ASSUME_NONNULL_BEGIN

@interface RNSFormSheetContentWrapperComponentView : RNSReactBaseView
Copy link
Copy Markdown
Contributor Author

@t0maboro t0maboro May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@t0maboro t0maboro force-pushed the @t0maboro/formsheet-presentation-state branch from d130afe to 53ecbf8 Compare May 27, 2026 07:09
Base automatically changed from @t0maboro/formsheet-presentation-state to main May 27, 2026 07:28
@t0maboro t0maboro force-pushed the @t0maboro/fit-to-contents branch from e3558da to 6121c7d Compare May 27, 2026 07:35
Copy link
Copy Markdown
Collaborator

@LKuchno LKuchno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Collaborator

@LKuchno LKuchno May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following new naming (described in RFC), if we are planning to implement e2e test I would change this line to:

Suggested change
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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Not sure if we need to export this?
  2. Other consts start with FORM_SHEET prefix - 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the bold here?

@@ -8,6 +9,9 @@
static BOOL RNSAreDetentsValid(const std::vector<double> &detents)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is [-1.0, -1.0] valid?

Comment on lines +33 to +43
- (id<RNSFormSheetContentWrapperDelegate>)resolveFormSheetContentWrapperDelegate
{
UIView *view = self.superview;
while (view != nil) {
if ([view isKindOfClass:[RNSFormSheetContentView class]]) {
return ((RNSFormSheetContentView *)view).contentWrapperDelegate;
}
view = view.superview;
}
return nil;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we should also be able to avoid awkward mountingTransactionDidMount.

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.

4 participants