Skip to content

Commit 27afc4d

Browse files
committed
adds support for column-based SplitViews on iOS 14+
1 parent 01023b9 commit 27afc4d

25 files changed

+291
-73
lines changed

e2e/SplitView.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe(':ios: SplitView', () => {
1717
});
1818

1919
it('push screen to master screen', async () => {
20-
await elementById(TestIDs.PUSH_MASTER_BTN).tap();
20+
await elementById(TestIDs.PUSH_PRIMARY_BTN).tap();
2121
await expect(elementByLabel('Pushed Screen')).toBeVisible();
2222
});
2323

lib/ios/RNNSplitViewController.m

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,44 @@
33

44
@implementation RNNSplitViewController
55

6+
- (instancetype)initWithLayoutInfo:(RNNLayoutInfo *)layoutInfo
7+
creator:(id<RNNComponentViewCreator>)creator
8+
options:(RNNNavigationOptions *)options
9+
defaultOptions:(RNNNavigationOptions *)defaultOptions
10+
presenter:(RNNBasePresenter *)presenter
11+
eventEmitter:(RNNEventEmitter *)eventEmitter
12+
childViewControllers:(NSArray *)childViewControllers {
13+
if (@available(iOS 14.0, *)) {
14+
NSString* style = options.splitView.style;
15+
if ([style isEqualToString:@"tripleColumn"]) {
16+
self = [self initWithStyle:UISplitViewControllerStyleTripleColumn];
17+
} else if ([style isEqualToString:@"doubleColumn"]) {
18+
self = [self initWithStyle:UISplitViewControllerStyleDoubleColumn];
19+
} else {
20+
self = [self init];
21+
}
22+
} else {
23+
// Fallback on earlier versions
24+
self = [self init];
25+
}
26+
self.options = options;
27+
self.defaultOptions = defaultOptions;
28+
self.layoutInfo = layoutInfo;
29+
self.creator = creator;
30+
self.eventEmitter = eventEmitter;
31+
self.presenter = presenter;
32+
[self loadChildren:childViewControllers];
33+
[self.presenter bindViewController:self];
34+
self.extendedLayoutIncludesOpaqueBars = YES;
35+
[self.presenter applyOptionsOnInit:self.resolveOptions];
36+
37+
return self;
38+
}
39+
640
- (void)setViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers {
741
[super setViewControllers:viewControllers];
8-
UIViewController<UISplitViewControllerDelegate> *masterViewController = viewControllers[0];
9-
self.delegate = masterViewController;
42+
UIViewController<UISplitViewControllerDelegate> *primaryViewController = viewControllers[0];
43+
self.delegate = primaryViewController;
1044
}
1145

1246
- (UIViewController *)getCurrentChild {

lib/ios/RNNSplitViewOptions.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
@property(nonatomic, strong) Number *minWidth;
88
@property(nonatomic, strong) Number *maxWidth;
99
@property(nonatomic, strong) NSString *primaryBackgroundStyle;
10+
@property(nonatomic, strong) NSString *style;
1011

1112
@end

lib/ios/RNNSplitViewOptions.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
1010
self.minWidth = [NumberParser parse:dict key:@"minWidth"];
1111
self.maxWidth = [NumberParser parse:dict key:@"maxWidth"];
1212
self.primaryBackgroundStyle = dict[@"primaryBackgroundStyle"];
13+
self.style = dict[@"style"];
1314
return self;
1415
}
1516

lib/ios/UISplitViewController+RNNOptions.m

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,30 @@ @implementation UISplitViewController (RNNOptions)
55

66
- (void)rnn_setDisplayMode:(NSString *)displayMode {
77
if ([displayMode isEqualToString:@"visible"]) {
8+
// deprecated since iOS 14
89
self.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
910
} else if ([displayMode isEqualToString:@"hidden"]) {
11+
// deprecated since iOS 14
1012
self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden;
1113
} else if ([displayMode isEqualToString:@"overlay"]) {
14+
// deprecated since iOS 14
1215
self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
16+
} else if ([displayMode isEqualToString:@"secondaryOnly"]) {
17+
self.preferredDisplayMode = UISplitViewControllerDisplayModeSecondaryOnly;
18+
} else if ([displayMode isEqualToString:@"oneBesideSecondary"]) {
19+
self.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary;
20+
} else if ([displayMode isEqualToString:@"oneOverSecondary"]) {
21+
self.preferredDisplayMode = UISplitViewControllerDisplayModeOneOverSecondary;
22+
} else if (@available(iOS 14.0, *)) {
23+
if ([displayMode isEqualToString:@"twoBesideSecondary"]) {
24+
self.preferredDisplayMode = UISplitViewControllerDisplayModeTwoBesideSecondary;
25+
} else if ([displayMode isEqualToString:@"twoDisplaceSecondary"]) {
26+
self.preferredDisplayMode = UISplitViewControllerDisplayModeTwoDisplaceSecondary;
27+
} else if ([displayMode isEqualToString:@"twoOverSecondary"]) {
28+
self.preferredDisplayMode = UISplitViewControllerDisplayModeTwoOverSecondary;
29+
} else {
30+
self.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic;
31+
}
1332
} else {
1433
self.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic;
1534
}

lib/src/Navigation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class NavigationRoot {
6363
this.componentWrapper,
6464
appRegistryService
6565
);
66-
this.layoutTreeParser = new LayoutTreeParser(this.uniqueIdProvider);
66+
this.layoutTreeParser = new LayoutTreeParser(this.uniqueIdProvider, new Deprecations());
6767
const optionsProcessor = new OptionsProcessor(
6868
this.store,
6969
this.uniqueIdProvider,

lib/src/commands/Commands.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { LayoutTreeParser } from './LayoutTreeParser';
77
import { LayoutTreeCrawler } from './LayoutTreeCrawler';
88
import { Store } from '../components/Store';
99
import { Commands } from './Commands';
10+
import { Deprecations } from './Deprecations';
1011
import { CommandsObserver } from '../events/CommandsObserver';
1112
import { NativeCommandsSender } from '../adapters/NativeCommandsSender';
1213
import { OptionsProcessor } from './OptionsProcessor';
@@ -42,7 +43,7 @@ describe('Commands', () => {
4243
uut = new Commands(
4344
mockedStore,
4445
instance(mockedNativeCommandsSender),
45-
new LayoutTreeParser(uniqueIdProvider),
46+
new LayoutTreeParser(uniqueIdProvider, new Deprecations()),
4647
new LayoutTreeCrawler(instance(mockedStore), optionsProcessor),
4748
commandsObserver,
4849
uniqueIdProvider,

lib/src/commands/Deprecations.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import once from 'lodash/once';
22
import get from 'lodash/get';
33
import each from 'lodash/each';
44
import { Platform } from 'react-native';
5+
import { Layout, LayoutSplitView } from 'react-native-navigation/interfaces/Layout';
56

67
export class Deprecations {
78
private deprecatedOptions: Array<{ key: string; showWarning: any }> = [
@@ -77,6 +78,17 @@ export class Deprecations {
7778
}
7879
}
7980

81+
public onParseLayout(api: Layout) {
82+
if (
83+
api.splitView &&
84+
Platform.OS === 'ios' &&
85+
typeof api.splitView.master !== 'undefined' &&
86+
typeof api.splitView.detail !== 'undefined'
87+
) {
88+
this.deprecateMasterDetailSplitView(api.splitView);
89+
}
90+
}
91+
8092
public onProcessDefaultOptions(_key: string, _parentOptions: Record<string, any>) {}
8193

8294
private deprecateSearchBarOptions = once((parentOptions: object) => {
@@ -97,4 +109,10 @@ export class Deprecations {
97109
parentOptions
98110
);
99111
});
112+
private deprecateMasterDetailSplitView = once((api: LayoutSplitView) => {
113+
console.warn(
114+
`using SplitView with master and detail is deprecated on iOS. For more information see https://github.com/wix/react-native-navigation/pull/6705`,
115+
api
116+
);
117+
});
100118
}

lib/src/commands/LayoutTreeParser.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import keys from 'lodash/keys';
22
import { LayoutTreeParser } from './LayoutTreeParser';
33
import { LayoutType } from './LayoutType';
4+
import { Deprecations } from './Deprecations';
45
import { Options } from '../interfaces/Options';
56
import { Layout } from '../interfaces/Layout';
67
import { UniqueIdProvider } from '../adapters/UniqueIdProvider';
@@ -13,7 +14,7 @@ describe('LayoutTreeParser', () => {
1314
beforeEach(() => {
1415
mockedUniqueIdProvider = mock(UniqueIdProvider);
1516
when(mockedUniqueIdProvider.generate(anything())).thenReturn('myUniqueId');
16-
uut = new LayoutTreeParser(instance(mockedUniqueIdProvider));
17+
uut = new LayoutTreeParser(instance(mockedUniqueIdProvider), new Deprecations());
1718
});
1819

1920
describe('parses into { type, data, children }', () => {
@@ -293,13 +294,13 @@ const complexLayout: Layout = {
293294

294295
const splitView: Layout = {
295296
splitView: {
296-
master: {
297+
primary: {
297298
stack: {
298299
children: [singleComponent],
299300
options,
300301
},
301302
},
302-
detail: stackWithTopBar,
303+
secondary: stackWithTopBar,
303304
options: optionsSplitView,
304305
},
305306
};

lib/src/commands/LayoutTreeParser.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { LayoutType } from './LayoutType';
22
import { LayoutNode } from './LayoutTreeCrawler';
3+
import { Deprecations } from './Deprecations';
34
import {
45
Layout,
56
LayoutTopTabs,
@@ -13,7 +14,7 @@ import {
1314
import { UniqueIdProvider } from '../adapters/UniqueIdProvider';
1415

1516
export class LayoutTreeParser {
16-
constructor(private uniqueIdProvider: UniqueIdProvider) {
17+
constructor(private uniqueIdProvider: UniqueIdProvider, private deprecations: Deprecations) {
1718
this.parse = this.parse.bind(this);
1819
}
1920

@@ -31,6 +32,10 @@ export class LayoutTreeParser {
3132
} else if (api.externalComponent) {
3233
return this.externalComponent(api.externalComponent);
3334
} else if (api.splitView) {
35+
if (api.splitView.master || api.splitView.detail) {
36+
// Deprecated
37+
this.deprecations.onParseLayout(api);
38+
}
3439
return this.splitView(api.splitView);
3540
}
3641
throw new Error(`unknown LayoutType "${Object.keys(api)}"`);
@@ -126,14 +131,31 @@ export class LayoutTreeParser {
126131
}
127132

128133
private splitView(api: LayoutSplitView): LayoutNode {
129-
const master = api.master ? this.parse(api.master) : undefined;
130-
const detail = api.detail ? this.parse(api.detail) : undefined;
131-
132134
return {
133135
id: api.id || this.uniqueIdProvider.generate(LayoutType.SplitView),
134136
type: LayoutType.SplitView,
135137
data: { options: api.options },
136-
children: master && detail ? [master, detail] : [],
138+
children: this.splitViewChildren(api),
137139
};
138140
}
141+
142+
private splitViewChildren(api: LayoutSplitView): LayoutNode[] {
143+
const children: LayoutNode[] = [];
144+
if (api.primary) {
145+
children.push(this.parse(api.primary));
146+
} else if (api.master) {
147+
// Deprecated -- treat as `primary`
148+
children.push(this.parse(api.master));
149+
}
150+
if (api.supplementary) {
151+
children.push(this.parse(api.supplementary));
152+
}
153+
if (api.secondary) {
154+
children.push(this.parse(api.secondary));
155+
} else if (api.detail) {
156+
// Deprecated -- treat as `secondary`
157+
children.push(this.parse(api.detail));
158+
}
159+
return children;
160+
}
139161
}

lib/src/interfaces/Layout.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,26 +105,43 @@ export interface LayoutSideMenu {
105105
options?: Options;
106106
}
107107

108-
export interface LayoutSplitView {
108+
export interface LayoutSplitViewCurrent {
109109
/**
110110
* Set ID of the stack so you can use Navigation.mergeOptions to
111111
* update options
112112
*/
113113
id?: string;
114114
/**
115-
* Set master layout (the smaller screen, sidebar)
115+
* Set primary layout
116116
*/
117-
master?: Layout;
117+
primary?: Layout;
118118
/**
119-
* Set detail layout (the larger screen, flexes)
119+
* Set supplementary layout (for 3 column layouts on iOS 14+)
120120
*/
121-
detail?: Layout;
121+
supplementary?: Layout;
122+
/**
123+
* Set secondary layout
124+
*/
125+
secondary?: Layout;
122126
/**
123127
* Configure split view
124128
*/
125129
options?: Options;
126130
}
127131

132+
export interface LayoutSplitViewDeprecated {
133+
/**
134+
* Set master layout (the smaller screen, sidebar)
135+
*/
136+
master?: Layout;
137+
/**
138+
* Set master layout (the smaller screen, sidebar)
139+
*/
140+
detail?: Layout;
141+
}
142+
143+
export type LayoutSplitView = LayoutSplitViewCurrent & LayoutSplitViewDeprecated;
144+
128145
export interface LayoutTopTabs {
129146
/**
130147
* Set the layout's id so Navigation.mergeOptions can be used to update options

lib/src/interfaces/Options.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,28 +77,44 @@ type Interpolation =
7777

7878
export interface OptionsSplitView {
7979
/**
80-
* Master view display mode
80+
* Primary view display mode.
81+
* The following options will only work on iOS 14+: twoBesideSecondary, twoDisplaceSecondary, twoOverSecondary
8182
* @default 'auto'
8283
*/
83-
displayMode?: 'auto' | 'visible' | 'hidden' | 'overlay';
84-
/**
85-
* Master view side. Leading is left. Trailing is right.
84+
displayMode?:
85+
| 'auto'
86+
| 'visible'
87+
| 'hidden'
88+
| 'overlay'
89+
| 'secondaryOnly'
90+
| 'oneBesideSecondary'
91+
| 'oneOverSecondary'
92+
| 'twoBesideSecondary' // iOS 14+ only
93+
| 'twoDisplaceSecondary' // iOS 14+ only
94+
| 'twoOverSecondary'; // iOS 14+ only
95+
/**
96+
* Primary view side. Leading is left. Trailing is right.
8697
* @default 'leading'
8798
*/
8899
primaryEdge?: 'leading' | 'trailing';
89100
/**
90-
* Set the minimum width of master view
101+
* Set the minimum width of primary view
91102
*/
92103
minWidth?: number;
93104
/**
94-
* Set the maximum width of master view
105+
* Set the maximum width of primary view
95106
*/
96107
maxWidth?: number;
97108
/**
98109
* Set background style of sidebar. Currently works for Mac Catalyst apps only.
99110
* @default 'none'
100111
*/
101112
primaryBackgroundStyle?: 'none' | 'sidebar';
113+
/**
114+
* Describe the number of columns the split view interface displays (iOS 14+)
115+
* @default 'unspecified'
116+
*/
117+
style?: 'unspecified' | 'doubleColumn' | 'tripleColumn';
102118
}
103119

104120
export interface OptionsStatusBar {

playground/src/screens/LayoutsScreen.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,24 +113,24 @@ export default class LayoutsScreen extends NavigationComponent {
113113
id: 'SPLITVIEW_ID',
114114
master: {
115115
stack: {
116-
id: 'MASTER_ID',
116+
id: 'PRIMARY_ID',
117117
children: [
118118
{
119119
component: {
120-
name: Screens.CocktailsListMasterScreen,
120+
name: Screens.CocktailsListPrimaryScreen,
121121
},
122122
},
123123
],
124124
},
125125
},
126126
detail: {
127127
stack: {
128-
id: 'DETAILS_ID',
128+
id: 'SECONDARY_ID',
129129
children: [
130130
{
131131
component: {
132132
id: 'DETAILS_COMPONENT_ID',
133-
name: Screens.CocktailDetailsScreen,
133+
name: Screens.CocktailSecondaryScreen,
134134
},
135135
},
136136
],
@@ -142,6 +142,9 @@ export default class LayoutsScreen extends NavigationComponent {
142142
},
143143
splitView: {
144144
displayMode: 'visible',
145+
minWidth: 375,
146+
maxWidth: 375,
147+
style: 'doubleColumn',
145148
},
146149
},
147150
},

0 commit comments

Comments
 (0)