Skip to content

Commit 346d86d

Browse files
authored
feat(iOS, SplitView): Add support for preferredDisplayMode prop (#3000)
## Description Closes software-mansion/react-native-screens-labs#223 This property specifies the display mode which will be preferred to use in SplitView, when the layout requirements are met. Possible DisplayMode values: - `secondaryOnly` - only the secondary column is displayed - `oneBesideSecondary` - a sidebar is displayed side-by-side with the secondary column - `twoBesideSecondary` - two sidebars are displayed side-by-side with the secondary column - `oneOverSecondary` - a one sidebar is displayed over the secondary column - `twoOverSecondary` - two sidebars are displayed over the secondary column - `twoDisplaceSecondary` - two sidebars are displacind the secondary column, moving it partially offscreen - `automatic` - leaving the choice to OS (default) ## Changes - Added prop definition in TS - Added enums and converters on the native side - Included new prop in `updateProps` and `resetProps` methods - Added warning for non-matching splitBehavior-displayMode pairs ## Test code and steps to reproduce `SplitViewBaseApp` was updated to cover the specific prop. https://github.com/user-attachments/assets/c722cfb3-f066-4b21-9e45-cbb156ac0220 ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes Closes: #223
1 parent 089885d commit 346d86d

File tree

8 files changed

+117
-71
lines changed

8 files changed

+117
-71
lines changed

apps/src/tests/TestSplitView/SplitViewBaseApp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const SplitViewBaseApp = () => {
2323
const [buttonState4, setButtonState4] = React.useState('Initial');
2424

2525
return (
26-
<SplitViewHost splitBehavior='tile' primaryEdge='leading'>
26+
<SplitViewHost displayMode='oneOverSecondary' primaryEdge='leading' splitBehavior='tile'>
2727
<SplitViewScreen>
2828
<View style={[styles.container, { backgroundColor: Colors.RedDark100 }]}>
2929
<TestButton setButtonState={setButtonState} />

ios/RNSEnums.h

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,6 @@ typedef NS_ENUM(NSInteger, RNSSearchBarPlacement) {
7373
RNSSearchBarPlacementStacked,
7474
};
7575

76-
#pragma mark - SplitView
77-
78-
typedef NS_ENUM(NSInteger, RNSSplitViewSplitBehavior) {
79-
RNSSplitViewSplitBehaviorAutomatic,
80-
RNSSplitViewSplitBehaviorDisplace,
81-
RNSSplitViewSplitBehaviorOverlay,
82-
RNSSplitViewSplitBehaviorTile,
83-
};
84-
85-
typedef NS_ENUM(NSInteger, RNSSplitViewPrimaryEdge) {
86-
RNSSplitViewPrimaryEdgeLeading,
87-
RNSSplitViewPrimaryEdgeTrailing
88-
};
89-
9076
// Redefinition of UIBlurEffectStyle. We need to represent additional case of `None`.
9177
typedef NS_ENUM(NSInteger, RNSBlurEffectStyle) {
9278
/// No blur effect should be visible

ios/conversion/RNSConversions-SplitView.mm

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,59 @@
44

55
#pragma mark RN Codegen enum to internal enum conversions
66

7-
RNSSplitViewSplitBehavior RNSSplitViewSplitBehaviorFromHostProp(
7+
UISplitViewControllerSplitBehavior SplitViewSplitBehaviorFromHostProp(
88
facebook::react::RNSSplitViewHostSplitBehavior splitBehavior)
99
{
1010
using enum facebook::react::RNSSplitViewHostSplitBehavior;
11-
11+
1212
switch (splitBehavior) {
1313
case Displace:
14-
return RNSSplitViewSplitBehaviorDisplace;
14+
return UISplitViewControllerSplitBehaviorDisplace;
1515
case Overlay:
16-
return RNSSplitViewSplitBehaviorOverlay;
16+
return UISplitViewControllerSplitBehaviorOverlay;
1717
case Tile:
18-
return RNSSplitViewSplitBehaviorTile;
18+
return UISplitViewControllerSplitBehaviorTile;
1919
case Automatic:
2020
default:
21-
return RNSSplitViewSplitBehaviorAutomatic;
21+
return UISplitViewControllerSplitBehaviorAutomatic;
2222
}
2323
}
2424

25-
RNSSplitViewPrimaryEdge RNSSplitViewPrimaryEdgeFromHostProp(
25+
UISplitViewControllerPrimaryEdge SplitViewPrimaryEdgeFromHostProp(
2626
facebook::react::RNSSplitViewHostPrimaryEdge primaryEdge)
2727
{
2828
using enum facebook::react::RNSSplitViewHostPrimaryEdge;
29-
29+
3030
switch (primaryEdge) {
3131
case Trailing:
32-
return RNSSplitViewPrimaryEdgeTrailing;
32+
return UISplitViewControllerPrimaryEdgeTrailing;
3333
case Leading:
3434
default:
35-
return RNSSplitViewPrimaryEdgeLeading;
36-
}
37-
}
38-
39-
#pragma mark Internal enum to UISplitViewController enum conversions
40-
41-
UISplitViewControllerSplitBehavior RNSSplitBehaviorToUISplitViewControllerSplitBehavior(
42-
RNSSplitViewSplitBehavior behavior)
43-
{
44-
switch (behavior) {
45-
case RNSSplitViewSplitBehaviorDisplace:
46-
return UISplitViewControllerSplitBehaviorDisplace;
47-
case RNSSplitViewSplitBehaviorOverlay:
48-
return UISplitViewControllerSplitBehaviorOverlay;
49-
case RNSSplitViewSplitBehaviorTile:
50-
return UISplitViewControllerSplitBehaviorTile;
51-
case RNSSplitViewSplitBehaviorAutomatic:
52-
default:
53-
return UISplitViewControllerSplitBehaviorAutomatic;
35+
return UISplitViewControllerPrimaryEdgeLeading;
5436
}
5537
}
5638

57-
UISplitViewControllerPrimaryEdge RNSPrimaryEdgeToUISplitViewControllerPrimaryEdge(
58-
RNSSplitViewPrimaryEdge primaryEdge)
39+
UISplitViewControllerDisplayMode SplitViewDisplayModeFromHostProp(
40+
facebook::react::RNSSplitViewHostDisplayMode displayMode)
5941
{
60-
switch (primaryEdge) {
61-
case RNSSplitViewPrimaryEdgeTrailing:
62-
return UISplitViewControllerPrimaryEdgeTrailing;
63-
case RNSSplitViewPrimaryEdgeLeading:
42+
using enum facebook::react::RNSSplitViewHostDisplayMode;
43+
44+
switch (displayMode) {
45+
case SecondaryOnly:
46+
return UISplitViewControllerDisplayModeSecondaryOnly;
47+
case OneBesideSecondary:
48+
return UISplitViewControllerDisplayModeOneBesideSecondary;
49+
case OneOverSecondary:
50+
return UISplitViewControllerDisplayModeOneOverSecondary;
51+
case TwoBesideSecondary:
52+
return UISplitViewControllerDisplayModeTwoBesideSecondary;
53+
case TwoOverSecondary:
54+
return UISplitViewControllerDisplayModeTwoOverSecondary;
55+
case TwoDisplaceSecondary:
56+
return UISplitViewControllerDisplayModeTwoDisplaceSecondary;
57+
case Automatic:
6458
default:
65-
return UISplitViewControllerPrimaryEdgeLeading;
59+
return UISplitViewControllerDisplayModeAutomatic;
6660
}
6761
}
6862

ios/conversion/RNSConversions.h

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,15 @@ UIOffset RNSBottomTabsScreenTabBarItemTitlePositionAdjustmentStruct(
3232

3333
#pragma mark SplitView
3434

35-
RNSSplitViewSplitBehavior RNSSplitViewSplitBehaviorFromHostProp(
36-
react::RNSSplitViewHostSplitBehavior);
35+
UISplitViewControllerSplitBehavior SplitViewSplitBehaviorFromHostProp(
36+
react::RNSSplitViewHostSplitBehavior behavior);
3737

38-
RNSSplitViewPrimaryEdge RNSSplitViewPrimaryEdgeFromHostProp(
39-
react::RNSSplitViewHostPrimaryEdge);
38+
UISplitViewControllerPrimaryEdge SplitViewPrimaryEdgeFromHostProp(
39+
react::RNSSplitViewHostPrimaryEdge primaryEdge);
4040

41-
UISplitViewControllerSplitBehavior
42-
RNSSplitBehaviorToUISplitViewControllerSplitBehavior(
43-
RNSSplitViewSplitBehavior behavior);
41+
UISplitViewControllerDisplayMode SplitViewDisplayModeFromHostProp(
42+
react::RNSSplitViewHostDisplayMode displayMode);
4443

45-
UISplitViewControllerPrimaryEdge
46-
RNSPrimaryEdgeToUISplitViewControllerPrimaryEdge(
47-
RNSSplitViewPrimaryEdge primaryEdge);
4844
}; // namespace rnscreens::conversion
4945

5046
#endif

ios/gamma/split-view/RNSSplitViewHostComponentView.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ NS_ASSUME_NONNULL_BEGIN
1818

1919
@interface RNSSplitViewHostComponentView ()
2020

21-
@property (nonatomic, readonly) RNSSplitViewSplitBehavior splitBehavior;
22-
@property (nonatomic, readonly) RNSSplitViewPrimaryEdge primaryEdge;
21+
@property (nonatomic, readonly) UISplitViewControllerSplitBehavior splitBehavior;
22+
@property (nonatomic, readonly) UISplitViewControllerPrimaryEdge primaryEdge;
23+
@property (nonatomic, readonly) UISplitViewControllerDisplayMode displayMode;
2324

2425
@end
2526

ios/gamma/split-view/RNSSplitViewHostComponentView.mm

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ - (void)resetProps
4646
static const auto defaultProps = std::make_shared<const react::RNSSplitViewHostProps>();
4747
_props = defaultProps;
4848

49-
_splitBehavior = RNSSplitViewSplitBehaviorAutomatic;
50-
_primaryEdge = RNSSplitViewPrimaryEdgeLeading;
49+
_splitBehavior = UISplitViewControllerSplitBehaviorAutomatic;
50+
_primaryEdge = UISplitViewControllerPrimaryEdgeLeading;
51+
_displayMode = UISplitViewControllerDisplayModeAutomatic;
5152
}
5253

5354
- (void)didMoveToWindow
@@ -140,15 +141,18 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
140141
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSSplitViewHostProps>(props);
141142

142143
if (oldComponentProps.splitBehavior != newComponentProps.splitBehavior) {
143-
_splitBehavior = rnscreens::conversion::RNSSplitViewSplitBehaviorFromHostProp(newComponentProps.splitBehavior);
144-
_controller.preferredSplitBehavior =
145-
rnscreens::conversion::RNSSplitBehaviorToUISplitViewControllerSplitBehavior(_splitBehavior);
144+
_splitBehavior = rnscreens::conversion::SplitViewSplitBehaviorFromHostProp(newComponentProps.splitBehavior);
145+
_controller.preferredSplitBehavior = _splitBehavior;
146146
}
147147

148148
if (oldComponentProps.primaryEdge != newComponentProps.primaryEdge) {
149-
_primaryEdge = rnscreens::conversion::RNSSplitViewPrimaryEdgeFromHostProp(newComponentProps.primaryEdge);
150-
_controller.primaryEdge =
151-
rnscreens::conversion::RNSPrimaryEdgeToUISplitViewControllerPrimaryEdge(_primaryEdge);
149+
_primaryEdge = rnscreens::conversion::SplitViewPrimaryEdgeFromHostProp(newComponentProps.primaryEdge);
150+
_controller.primaryEdge = _primaryEdge;
151+
}
152+
153+
if (oldComponentProps.displayMode != newComponentProps.displayMode) {
154+
_displayMode = rnscreens::conversion::SplitViewDisplayModeFromHostProp(newComponentProps.displayMode);
155+
_controller.preferredDisplayMode = _displayMode;
152156
}
153157

154158
[super updateProps:props oldProps:oldProps];

src/components/gamma/SplitViewHost.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,76 @@ import type { ViewProps } from 'react-native';
44
import SplitViewHostNativeComponent from '../../fabric/gamma/SplitViewHostNativeComponent';
55
import type {
66
NativeProps,
7+
SplitViewDisplayMode,
78
SplitViewSplitBehavior,
89
} from '../../fabric/gamma/SplitViewHostNativeComponent';
910

1011
export type SplitViewNativeProps = NativeProps & {
1112
// SplitView appearance
1213

14+
displayMode?: SplitViewDisplayMode;
1315
splitBehavior?: SplitViewSplitBehavior;
1416
};
1517

1618
type SplitViewHostProps = {
1719
children?: ViewProps['children'];
1820
} & SplitViewNativeProps;
1921

20-
function SplitViewHost({ children, splitBehavior, primaryEdge }: SplitViewHostProps) {
22+
// According to the UIKit documentation: https://developer.apple.com/documentation/uikit/uisplitviewcontroller/displaymode-swift.enum
23+
// Only specific pairs for displayMode - splitBehavior are valid and others may lead to unexpected results.
24+
// Therefore, we're adding check on the JS side to return a feedback to the client when that pairing isn't valid.
25+
// However, we're not blocking these props to be set on the native side, because it doesn't crash, just the result or transitions may not work as expected.
26+
const displayModeForSplitViewCompatibilityMap: Record<
27+
SplitViewSplitBehavior,
28+
SplitViewDisplayMode[]
29+
> = {
30+
tile: ['secondaryOnly', 'oneBesideSecondary', 'twoBesideSecondary'],
31+
overlay: ['secondaryOnly', 'oneOverSecondary', 'twoOverSecondary'],
32+
displace: ['secondaryOnly', 'oneBesideSecondary', 'twoDisplaceSecondary'],
33+
automatic: [], // placeholder for satisfying types; we'll handle it specially in logic
34+
};
35+
36+
const isValidDisplayModeForSplitBehavior = (
37+
displayMode: SplitViewDisplayMode,
38+
splitBehavior: SplitViewSplitBehavior,
39+
) => {
40+
if (splitBehavior === 'automatic') {
41+
// for automatic we cannot easily verify the compatibility, because it depends on the system preference for display mode, therefore we're assuming that 'automatic' has only valid combinations
42+
return true;
43+
}
44+
return displayModeForSplitViewCompatibilityMap[splitBehavior].includes(
45+
displayMode,
46+
);
47+
};
48+
49+
function SplitViewHost({
50+
children,
51+
displayMode,
52+
primaryEdge,
53+
splitBehavior,
54+
}: SplitViewHostProps) {
55+
React.useEffect(() => {
56+
if (displayMode && splitBehavior) {
57+
const isValid = isValidDisplayModeForSplitBehavior(
58+
displayMode,
59+
splitBehavior,
60+
);
61+
if (!isValid) {
62+
const validDisplayModes =
63+
displayModeForSplitViewCompatibilityMap[splitBehavior];
64+
console.warn(
65+
`Invalid display mode "${displayMode}" for split behavior "${splitBehavior}".` +
66+
`\nValid modes for "${splitBehavior}" are: ${validDisplayModes.join(
67+
', ',
68+
)}.`,
69+
);
70+
}
71+
}
72+
}, [displayMode, splitBehavior]);
73+
2174
return (
2275
<SplitViewHostNativeComponent
76+
displayMode={displayMode}
2377
style={styles.container}
2478
splitBehavior={splitBehavior}
2579
primaryEdge={primaryEdge}>

src/fabric/gamma/SplitViewHostNativeComponent.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,19 @@ export type SplitViewSplitBehavior =
1212

1313
export type SplitViewPrimaryEdge = 'leading' | 'trailing';
1414

15+
export type SplitViewDisplayMode =
16+
| 'automatic'
17+
| 'secondaryOnly'
18+
| 'oneBesideSecondary'
19+
| 'oneOverSecondary'
20+
| 'twoBesideSecondary'
21+
| 'twoOverSecondary'
22+
| 'twoDisplaceSecondary';
23+
1524
export interface NativeProps extends ViewProps {
1625
// Appearance
26+
27+
displayMode?: WithDefault<SplitViewDisplayMode, 'automatic'>;
1728
splitBehavior?: WithDefault<SplitViewSplitBehavior, 'automatic'>;
1829
primaryEdge?: WithDefault<SplitViewPrimaryEdge, 'leading'>;
1930
}

0 commit comments

Comments
 (0)