From 3096fe0e2b8e7cfe78d09d3566e5ce8ece367032 Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Fri, 16 Oct 2026 17:32:43 -0700 Subject: [PATCH 1/2] fix(ios): use horizontal bars in sheets on iPhone Duo --- ios/legacy/RNSScreen.mm | 15 +++++++++++++++ .../form-sheet/RNSFormSheetContentController.mm | 9 +++++++++ package.json | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ios/legacy/RNSScreen.mm b/ios/legacy/RNSScreen.mm index 67ceb528c8..b96250019e 100644 --- a/ios/legacy/RNSScreen.mm +++ b/ios/legacy/RNSScreen.mm @@ -1450,6 +1450,21 @@ @implementation RNSScreen { #pragma mark - Common +#if RNS_IPHONE_OS_VERSION_AVAILABLE(27_1) && !TARGET_OS_TV && !TARGET_OS_VISION +// Navigation sheets use horizontal bars, matching the standalone FormSheet. +- (UIVerticalBarBehavior)preferredVerticalBarBehavior API_AVAILABLE(ios(27.1)) +{ + switch (self.screenView.stackPresentation) { + case RNSScreenStackPresentationModal: + case RNSScreenStackPresentationPageSheet: + case RNSScreenStackPresentationFormSheet: + return UIVerticalBarBehaviorDisabled; + default: + return [super preferredVerticalBarBehavior]; + } +} +#endif + - (instancetype)initWithView:(UIView *)view { if (self = [super init]) { diff --git a/ios/modals/form-sheet/RNSFormSheetContentController.mm b/ios/modals/form-sheet/RNSFormSheetContentController.mm index a6e34d25b4..8c90a0e368 100644 --- a/ios/modals/form-sheet/RNSFormSheetContentController.mm +++ b/ios/modals/form-sheet/RNSFormSheetContentController.mm @@ -5,6 +5,7 @@ #import "RNSFormSheetUpdateCoordinator.h" #import "RNSFormSheetUpdateFlags.h" #import "RNSPresentationSourceProvider.h" +#import "RNSDefines.h" #import #import @@ -43,6 +44,14 @@ - (RNSFormSheetContentView *)contentView return static_cast(self.view); } +#if RNS_IPHONE_OS_VERSION_AVAILABLE(27_1) && !TARGET_OS_TV && !TARGET_OS_VISION +// Short sheets should not reserve an empty control rail on iPhone Duo. +- (UIVerticalBarBehavior)preferredVerticalBarBehavior API_AVAILABLE(ios(27.1)) +{ + return UIVerticalBarBehaviorDisabled; +} +#endif + #pragma mark - UIKit callbacks - (void)loadView diff --git a/package.json b/package.json index 4783005d26..c5b2b267a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-screens", - "version": "5.0.0-t3.27", + "version": "5.0.0-t3.28", "description": "Native navigation primitives for your React Native app.", "scripts": { "submodules": "git submodule update --init --recursive && (cd react-navigation && yarn && yarn build && cd ../)", From f0d4b311e088f41cd74d7970017769f49f14e6cc Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Fri, 16 Oct 2026 18:08:45 -0700 Subject: [PATCH 2/2] fix(ios): make horizontal sheet bars opt-in --- .../swmansion/rnscreens/legacy/ScreenViewManager.kt | 5 +++++ .../formsheet/host/FormSheetHostViewManager.kt | 5 +++++ ios/legacy/RNSScreen.h | 1 + ios/legacy/RNSScreen.mm | 13 ++++++++++++- .../form-sheet/RNSFormSheetContentController.mm | 6 ++++-- .../form-sheet/RNSFormSheetHostComponentView.h | 1 + .../form-sheet/RNSFormSheetHostComponentView.mm | 12 ++++++++++++ ios/modals/form-sheet/RNSFormSheetProviders.h | 1 + package.json | 2 +- src/components/modals/form-sheet/FormSheet.types.ts | 10 ++++++++++ src/fabric/legacy/ModalScreenNativeComponent.ts | 1 + src/fabric/legacy/ScreenNativeComponent.ts | 1 + .../form-sheet/FormSheetHostNativeComponent.ts | 1 + src/legacy/types.ts | 9 +++++++++ 14 files changed, 64 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/legacy/ScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/legacy/ScreenViewManager.kt index edbe695f23..fd47f1fb78 100644 --- a/android/src/main/java/com/swmansion/rnscreens/legacy/ScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/legacy/ScreenViewManager.kt @@ -328,6 +328,11 @@ open class ScreenViewManager : value: Boolean, ) = Unit + override fun setSheetPrefersHorizontalBars( + view: Screen?, + value: Boolean, + ) = Unit + // END mark: iOS-only override fun setAndroidResetScreenShadowStateOnOrientationChangeEnabled( diff --git a/android/src/main/java/com/swmansion/rnscreens/react/modals/formsheet/host/FormSheetHostViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/react/modals/formsheet/host/FormSheetHostViewManager.kt index 3f7c253c91..14cc616258 100644 --- a/android/src/main/java/com/swmansion/rnscreens/react/modals/formsheet/host/FormSheetHostViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/react/modals/formsheet/host/FormSheetHostViewManager.kt @@ -91,6 +91,11 @@ class FormSheetHostViewManager : view.prefersGrabberVisible = value } + override fun setPrefersHorizontalBars( + view: FormSheetHost, + value: Boolean, + ) = Unit // iOS-only bar placement preference. + override fun setPreferredCornerRadius( view: FormSheetHost, value: Float, diff --git a/ios/legacy/RNSScreen.h b/ios/legacy/RNSScreen.h index 49ff50b2dd..8398305e92 100644 --- a/ios/legacy/RNSScreen.h +++ b/ios/legacy/RNSScreen.h @@ -117,6 +117,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSArray *sheetAllowedDetents; @property (nonatomic) NSNumber *sheetLargestUndimmedDetent; @property (nonatomic) BOOL sheetGrabberVisible; +@property (nonatomic) BOOL sheetPrefersHorizontalBars; @property (nonatomic) CGFloat sheetCornerRadius; @property (nonatomic) NSInteger sheetInitialDetent; @property (nonatomic) BOOL sheetExpandsWhenScrolledToEdge; diff --git a/ios/legacy/RNSScreen.mm b/ios/legacy/RNSScreen.mm index b96250019e..98405fd8ed 100644 --- a/ios/legacy/RNSScreen.mm +++ b/ios/legacy/RNSScreen.mm @@ -100,6 +100,7 @@ - (void)initCommonProps _shouldUpdateScrollEdgeEffects = NO; #if !TARGET_OS_TV _sheetExpandsWhenScrolledToEdge = YES; + _sheetPrefersHorizontalBars = NO; #endif // !TARGET_OS_TV _sheetsScrollView = nil; _sheetsScrollViewTag = 0; @@ -1313,6 +1314,14 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: } [self setSheetGrabberVisible:newScreenProps.sheetGrabberVisible]; + if (_sheetPrefersHorizontalBars != newScreenProps.sheetPrefersHorizontalBars) { + _sheetPrefersHorizontalBars = newScreenProps.sheetPrefersHorizontalBars; +#if RNS_IPHONE_OS_VERSION_AVAILABLE(27_1) && !TARGET_OS_VISION + if (@available(iOS 27.1, *)) { + [_controller setNeedsUpdateOfVerticalBarConfiguration]; + } +#endif + } [self setSheetCornerRadius:newScreenProps.sheetCornerRadius]; [self setSheetExpandsWhenScrolledToEdge:newScreenProps.sheetExpandsWhenScrolledToEdge]; @@ -1451,9 +1460,11 @@ @implementation RNSScreen { #pragma mark - Common #if RNS_IPHONE_OS_VERSION_AVAILABLE(27_1) && !TARGET_OS_TV && !TARGET_OS_VISION -// Navigation sheets use horizontal bars, matching the standalone FormSheet. - (UIVerticalBarBehavior)preferredVerticalBarBehavior API_AVAILABLE(ios(27.1)) { + if (!self.screenView.sheetPrefersHorizontalBars) { + return [super preferredVerticalBarBehavior]; + } switch (self.screenView.stackPresentation) { case RNSScreenStackPresentationModal: case RNSScreenStackPresentationPageSheet: diff --git a/ios/modals/form-sheet/RNSFormSheetContentController.mm b/ios/modals/form-sheet/RNSFormSheetContentController.mm index 8c90a0e368..bd78aae7d6 100644 --- a/ios/modals/form-sheet/RNSFormSheetContentController.mm +++ b/ios/modals/form-sheet/RNSFormSheetContentController.mm @@ -45,10 +45,12 @@ - (RNSFormSheetContentView *)contentView } #if RNS_IPHONE_OS_VERSION_AVAILABLE(27_1) && !TARGET_OS_TV && !TARGET_OS_VISION -// Short sheets should not reserve an empty control rail on iPhone Duo. - (UIVerticalBarBehavior)preferredVerticalBarBehavior API_AVAILABLE(ios(27.1)) { - return UIVerticalBarBehaviorDisabled; + if (self.appearanceProvider.prefersHorizontalBars) { + return UIVerticalBarBehaviorDisabled; + } + return [super preferredVerticalBarBehavior]; } #endif diff --git a/ios/modals/form-sheet/RNSFormSheetHostComponentView.h b/ios/modals/form-sheet/RNSFormSheetHostComponentView.h index bc1367191d..9e9beedca5 100644 --- a/ios/modals/form-sheet/RNSFormSheetHostComponentView.h +++ b/ios/modals/form-sheet/RNSFormSheetHostComponentView.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL isOpen; @property (nonatomic, readonly) BOOL prefersGrabberVisible; +@property (nonatomic, readonly) BOOL prefersHorizontalBars; @property (nonatomic, readonly) CGFloat preferredCornerRadius; @property (nonatomic, readonly) NSInteger largestUndimmedDetentIndex; @property (nonatomic, readonly) NSInteger initialDetentIndex; diff --git a/ios/modals/form-sheet/RNSFormSheetHostComponentView.mm b/ios/modals/form-sheet/RNSFormSheetHostComponentView.mm index e2d0cad04d..bc439a3138 100644 --- a/ios/modals/form-sheet/RNSFormSheetHostComponentView.mm +++ b/ios/modals/form-sheet/RNSFormSheetHostComponentView.mm @@ -1,4 +1,5 @@ #import "RNSFormSheetHostComponentView.h" +#import "RNSDefines.h" #import "RNSFormSheetContentController.h" #import "RNSFormSheetContentView.h" #import "RNSFormSheetContentWrapperComponentView.h" @@ -36,6 +37,7 @@ @implementation RNSFormSheetHostComponentView { BOOL _isOpen; std::vector _detents; BOOL _prefersGrabberVisible; + BOOL _prefersHorizontalBars; CGFloat _preferredCornerRadius; NSInteger _largestUndimmedDetentIndex; NSInteger _initialDetentIndex; @@ -71,6 +73,7 @@ - (void)resetProps _isOpen = NO; _detents = {}; _prefersGrabberVisible = NO; + _prefersHorizontalBars = NO; _preferredCornerRadius = -1.0; _largestUndimmedDetentIndex = kRNSFormSheetAlwaysDimmed; _initialDetentIndex = 0; @@ -274,6 +277,15 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props [_controller setNeedsAppearanceUpdate]; } + if (oldComponentProps.prefersHorizontalBars != newComponentProps.prefersHorizontalBars) { + _prefersHorizontalBars = newComponentProps.prefersHorizontalBars; +#if RNS_IPHONE_OS_VERSION_AVAILABLE(27_1) && !TARGET_OS_TV && !TARGET_OS_VISION + if (@available(iOS 27.1, *)) { + [_controller setNeedsUpdateOfVerticalBarConfiguration]; + } +#endif + } + if (oldComponentProps.preferredCornerRadius != newComponentProps.preferredCornerRadius) { _preferredCornerRadius = newComponentProps.preferredCornerRadius; [_controller setNeedsAppearanceUpdate]; diff --git a/ios/modals/form-sheet/RNSFormSheetProviders.h b/ios/modals/form-sheet/RNSFormSheetProviders.h index 66ff2e2898..588d5878c2 100644 --- a/ios/modals/form-sheet/RNSFormSheetProviders.h +++ b/ios/modals/form-sheet/RNSFormSheetProviders.h @@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol RNSFormSheetAppearanceProvider - (BOOL)prefersGrabberVisible; +- (BOOL)prefersHorizontalBars; - (CGFloat)preferredCornerRadius; - (NSInteger)largestUndimmedDetentIndex; - (nullable UIColor *)nativeContainerBackgroundColor; diff --git a/package.json b/package.json index c5b2b267a5..599d095126 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-screens", - "version": "5.0.0-t3.28", + "version": "5.0.0-t3.29", "description": "Native navigation primitives for your React Native app.", "scripts": { "submodules": "git submodule update --init --recursive && (cd react-navigation && yarn && yarn build && cd ../)", diff --git a/src/components/modals/form-sheet/FormSheet.types.ts b/src/components/modals/form-sheet/FormSheet.types.ts index 98b45c6e15..fb818e1fa8 100644 --- a/src/components/modals/form-sheet/FormSheet.types.ts +++ b/src/components/modals/form-sheet/FormSheet.types.ts @@ -63,6 +63,16 @@ export interface FormSheetProps { */ prefersGrabberVisible?: boolean | undefined; + /** + * Requests horizontal bars instead of the vertical bar on iPhone Duo. + * Leave unset to let UIKit place native controls in the vertical bar. + * + * @default false + * @platform ios + * @supported iOS 27.1 or higher + */ + prefersHorizontalBars?: boolean | undefined; + /** * @summary The corner radius that the sheet will attempt to present with. * diff --git a/src/fabric/legacy/ModalScreenNativeComponent.ts b/src/fabric/legacy/ModalScreenNativeComponent.ts index 1633ff0f12..b56b0d62a2 100644 --- a/src/fabric/legacy/ModalScreenNativeComponent.ts +++ b/src/fabric/legacy/ModalScreenNativeComponent.ts @@ -92,6 +92,7 @@ export interface NativeProps extends ViewProps { sheetAllowedDetents?: number[] | undefined; sheetLargestUndimmedDetent?: CT.WithDefault; sheetGrabberVisible?: CT.WithDefault; + sheetPrefersHorizontalBars?: CT.WithDefault; sheetCornerRadius?: CT.WithDefault; sheetExpandsWhenScrolledToEdge?: CT.WithDefault; sheetInitialDetent?: CT.WithDefault; diff --git a/src/fabric/legacy/ScreenNativeComponent.ts b/src/fabric/legacy/ScreenNativeComponent.ts index 0d0085c7fa..1fb1cb3c0c 100644 --- a/src/fabric/legacy/ScreenNativeComponent.ts +++ b/src/fabric/legacy/ScreenNativeComponent.ts @@ -92,6 +92,7 @@ export interface NativeProps extends ViewProps { sheetAllowedDetents?: number[] | undefined; sheetLargestUndimmedDetent?: CT.WithDefault; sheetGrabberVisible?: CT.WithDefault; + sheetPrefersHorizontalBars?: CT.WithDefault; sheetCornerRadius?: CT.WithDefault; sheetExpandsWhenScrolledToEdge?: CT.WithDefault; sheetInitialDetent?: CT.WithDefault; diff --git a/src/fabric/modals/form-sheet/FormSheetHostNativeComponent.ts b/src/fabric/modals/form-sheet/FormSheetHostNativeComponent.ts index 29b785373a..5d9edb44ce 100644 --- a/src/fabric/modals/form-sheet/FormSheetHostNativeComponent.ts +++ b/src/fabric/modals/form-sheet/FormSheetHostNativeComponent.ts @@ -20,6 +20,7 @@ interface NativeProps extends ViewProps { isOpen?: CT.WithDefault; detents?: CT.Double[] | undefined; prefersGrabberVisible?: CT.WithDefault; + prefersHorizontalBars?: CT.WithDefault; preferredCornerRadius?: CT.WithDefault; largestUndimmedDetentIndex?: CT.WithDefault; initialDetentIndex?: CT.WithDefault; diff --git a/src/legacy/types.ts b/src/legacy/types.ts index ed29506c10..3f580790e8 100644 --- a/src/legacy/types.ts +++ b/src/legacy/types.ts @@ -454,6 +454,15 @@ export interface ScreenProps extends ViewProps { * @platform ios */ sheetGrabberVisible?: boolean | undefined; + /** + * Requests horizontal bars instead of the vertical bar for modal, page-sheet, + * and form-sheet presentations on iPhone Duo. Defaults to `false`, preserving + * UIKit's automatic bar placement. + * + * @platform ios + * @supported iOS 27.1 or higher + */ + sheetPrefersHorizontalBars?: boolean | undefined; /** * The largest sheet detent for which a view underneath won't be dimmed. * Works only when `stackPresentation` is set to `formSheet`.