From 5ec4004e6514e536e0fa2cb3cfec0febdcd0f6bf Mon Sep 17 00:00:00 2001 From: JP Date: Wed, 21 Oct 2020 11:17:16 +0100 Subject: [PATCH] adds support for column-based SplitViews on iOS 14+ --- lib/ios/RNNSplitViewController.m | 37 +++++++ lib/ios/UISplitViewController+RNNOptions.m | 37 +++++-- lib/src/commands/LayoutTreeParser.test.ts | 55 +++++++++-- lib/src/commands/LayoutTreeParser.ts | 31 ++++-- lib/src/interfaces/Layout.ts | 28 +++++- lib/src/interfaces/Options.ts | 15 ++- website/docs/api/layout-splitView.mdx | 110 +++++++++++++++++++++ website/docs/api/options-splitView.mdx | 16 ++- 8 files changed, 296 insertions(+), 33 deletions(-) diff --git a/lib/ios/RNNSplitViewController.m b/lib/ios/RNNSplitViewController.m index dbbb1d49921..03969cf3ce7 100644 --- a/lib/ios/RNNSplitViewController.m +++ b/lib/ios/RNNSplitViewController.m @@ -3,6 +3,43 @@ @implementation RNNSplitViewController +- (instancetype)initWithLayoutInfo:(RNNLayoutInfo *)layoutInfo + creator:(id)creator + options:(RNNNavigationOptions *)options + defaultOptions:(RNNNavigationOptions *)defaultOptions + presenter:(RNNBasePresenter *)presenter + eventEmitter:(RNNEventEmitter *)eventEmitter + childViewControllers:(NSArray *)childViewControllers { + if (@available(iOS 14.0, *)) { + NSString* displayMode = options.splitView.displayMode; + NSArray* possibleDisplayModes = [NSArray arrayWithObjects: @"secondaryOnly", @"oneBesideSecondary", @"oneOverSecondary", @"twoBesideSecondary", @"twoDisplaceSecondary", @"twoOverSecondary", nil]; + + if (childViewControllers.count == 3 && [possibleDisplayModes containsObject:displayMode]) { + self = [self initWithStyle:UISplitViewControllerStyleTripleColumn]; + } else if (childViewControllers.count == 2 && [possibleDisplayModes containsObject:displayMode]) { + self = [self initWithStyle:UISplitViewControllerStyleDoubleColumn]; + } else { + // Fallback on iOS 14 but without a new displayMode + self = [self init]; + } + } else { + // Fallback on earlier versions + self = [self init]; + } + self.options = options; + self.defaultOptions = defaultOptions; + self.layoutInfo = layoutInfo; + self.creator = creator; + self.eventEmitter = eventEmitter; + self.presenter = presenter; + [self.presenter bindViewController:self]; + self.extendedLayoutIncludesOpaqueBars = YES; + [self loadChildren:childViewControllers]; + [self.presenter applyOptionsOnInit:self.resolveOptions]; + + return self; +} + - (void)setViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers { [super setViewControllers:viewControllers]; UIViewController* masterViewController = viewControllers[0]; diff --git a/lib/ios/UISplitViewController+RNNOptions.m b/lib/ios/UISplitViewController+RNNOptions.m index dbe7a136ac1..49301d97122 100644 --- a/lib/ios/UISplitViewController+RNNOptions.m +++ b/lib/ios/UISplitViewController+RNNOptions.m @@ -4,15 +4,34 @@ @implementation UISplitViewController (RNNOptions) - (void)rnn_setDisplayMode:(NSString *)displayMode { - if ([displayMode isEqualToString:@"visible"]) { - self.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; - } else if ([displayMode isEqualToString:@"hidden"]) { - self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden; - } else if ([displayMode isEqualToString:@"overlay"]) { - self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay; - } else { - self.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic; - } + if ([displayMode isEqualToString:@"visible"]) { + // deprecated since iOS 14 + self.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; + } else if ([displayMode isEqualToString:@"hidden"]) { + // deprecated since iOS 14 + self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden; + } else if ([displayMode isEqualToString:@"overlay"]) { + // deprecated since iOS 14 + self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay; + } else if ([displayMode isEqualToString:@"secondaryOnly"]) { + self.preferredDisplayMode = UISplitViewControllerDisplayModeSecondaryOnly; + } else if ([displayMode isEqualToString:@"oneBesideSecondary"]) { + self.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary; + } else if ([displayMode isEqualToString:@"oneOverSecondary"]) { + self.preferredDisplayMode = UISplitViewControllerDisplayModeOneOverSecondary; + } else if (@available(iOS 14.0, *)) { + if ([displayMode isEqualToString:@"twoBesideSecondary"]) { + self.preferredDisplayMode = UISplitViewControllerDisplayModeTwoBesideSecondary; + } else if ([displayMode isEqualToString:@"twoDisplaceSecondary"]) { + self.preferredDisplayMode = UISplitViewControllerDisplayModeTwoDisplaceSecondary; + } else if ([displayMode isEqualToString:@"twoOverSecondary"]) { + self.preferredDisplayMode = UISplitViewControllerDisplayModeTwoOverSecondary; + } else { + self.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic; + } + } else { + self.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic; + } } - (void)rnn_setPrimaryEdge:(NSString *)primaryEdge { diff --git a/lib/src/commands/LayoutTreeParser.test.ts b/lib/src/commands/LayoutTreeParser.test.ts index 1b5e5c50fb2..9485cd6883f 100644 --- a/lib/src/commands/LayoutTreeParser.test.ts +++ b/lib/src/commands/LayoutTreeParser.test.ts @@ -139,14 +139,34 @@ describe('LayoutTreeParser', () => { expect(result.children[1].children[0].children[2].type).toEqual('Stack'); }); - it('split view', () => { - const result = uut.parse(LayoutExamples.splitView); - const master = uut.parse(LayoutExamples.splitView.splitView!.master!); - const detail = uut.parse(LayoutExamples.splitView.splitView!.detail!); + it('classic split view', () => { + const result = uut.parse(LayoutExamples.classicSplitView); + const api = LayoutExamples.classicSplitView.splitView!; + if (!('primary' in api)) { + // api is ClassicLayoutSplitView + const master = uut.parse(api.master!); + const detail = uut.parse(api.detail!); - expect(result.type).toEqual('SplitView'); - expect(result.children[0]).toEqual(master); - expect(result.children[1]).toEqual(detail); + expect(result.type).toEqual('SplitView'); + expect(result.children[0]).toEqual(master); + expect(result.children[1]).toEqual(detail); + } + }); + + it('modern split view', () => { + const result = uut.parse(LayoutExamples.modernSplitView); + const api = LayoutExamples.modernSplitView.splitView!; + if ('primary' in api) { + // api is ModernLayoutSplitView + const primary = uut.parse(api.primary); + const supplementary = uut.parse(api.supplementary!); + const secondary = uut.parse(api.secondary); + + expect(result.type).toEqual('SplitView'); + expect(result.children[0]).toEqual(primary); + expect(result.children[1]).toEqual(supplementary); + expect(result.children[2]).toEqual(secondary); + } }); }); @@ -158,7 +178,7 @@ describe('LayoutTreeParser', () => { expect( uut.parse({ sideMenu: { options, center: { component: { name: 'lool' } } } }).data.options ).toBe(options); - expect(uut.parse(LayoutExamples.splitView).data.options).toBe(optionsSplitView); + expect(uut.parse(LayoutExamples.classicSplitView).data.options).toBe(optionsSplitView); }); it('pass user provided id as is', () => { @@ -291,7 +311,7 @@ const complexLayout: Layout = { }, }; -const splitView: Layout = { +const classicSplitView: Layout = { splitView: { master: { stack: { @@ -304,6 +324,20 @@ const splitView: Layout = { }, }; +const modernSplitView: Layout = { + splitView: { + primary: { + stack: { + children: [singleComponent], + options, + }, + }, + supplementary: stackWithTopBar, + secondary: stackWithTopBar, + options: optionsSplitView, + }, +}; + const LayoutExamples = { passProps, options, @@ -314,5 +348,6 @@ const LayoutExamples = { topTabs, complexLayout, externalComponent, - splitView, + classicSplitView, + modernSplitView, }; diff --git a/lib/src/commands/LayoutTreeParser.ts b/lib/src/commands/LayoutTreeParser.ts index 57d75350b6c..f9b1eb9d20b 100644 --- a/lib/src/commands/LayoutTreeParser.ts +++ b/lib/src/commands/LayoutTreeParser.ts @@ -126,14 +126,29 @@ export class LayoutTreeParser { } private splitView(api: LayoutSplitView): LayoutNode { - const master = api.master ? this.parse(api.master) : undefined; - const detail = api.detail ? this.parse(api.detail) : undefined; + if (!('primary' in api)) { + // api is ClassicLayoutSplitView + const master = api.master ? this.parse(api.master) : undefined; + const detail = api.detail ? this.parse(api.detail) : undefined; - return { - id: api.id || this.uniqueIdProvider.generate(LayoutType.SplitView), - type: LayoutType.SplitView, - data: { options: api.options }, - children: master && detail ? [master, detail] : [], - }; + return { + id: api.id || this.uniqueIdProvider.generate(LayoutType.SplitView), + type: LayoutType.SplitView, + data: { options: api.options }, + children: master && detail ? [master, detail] : [], + }; + } else { + // api is ModernLayoutSplitView + const primary = this.parse(api.primary); + const supplementary = api.supplementary ? this.parse(api.secondary) : undefined; + const secondary = this.parse(api.secondary); + + return { + id: api.id || this.uniqueIdProvider.generate(LayoutType.SplitView), + type: LayoutType.SplitView, + data: { options: api.options }, + children: supplementary ? [primary, supplementary, secondary] : [primary, secondary], + }; + } } } diff --git a/lib/src/interfaces/Layout.ts b/lib/src/interfaces/Layout.ts index aee17a02ded..17aa3bafd54 100644 --- a/lib/src/interfaces/Layout.ts +++ b/lib/src/interfaces/Layout.ts @@ -105,7 +105,7 @@ export interface LayoutSideMenu { options?: Options; } -export interface LayoutSplitView { +interface ClassicLayoutSplitView { /** * Set ID of the stack so you can use Navigation.mergeOptions to * update options @@ -125,6 +125,32 @@ export interface LayoutSplitView { options?: Options; } +interface ModernLayoutSplitView { + /** + * Set ID of the stack so you can use Navigation.mergeOptions to + * update options + */ + id?: string; + /** + * Set primary layout + */ + primary: Layout; + /** + * Set supplementary layout (for 3 column layouts on iOS 14+) + */ + supplementary?: Layout; + /** + * Set secondary layout + */ + secondary: Layout; + /** + * Configure split view + */ + options?: Options; +} + +export type LayoutSplitView = ClassicLayoutSplitView | ModernLayoutSplitView; + export interface LayoutTopTabs { /** * Set the layout's id so Navigation.mergeOptions can be used to update options diff --git a/lib/src/interfaces/Options.ts b/lib/src/interfaces/Options.ts index fa34cb79339..6d2292291e3 100644 --- a/lib/src/interfaces/Options.ts +++ b/lib/src/interfaces/Options.ts @@ -77,10 +77,21 @@ type Interpolation = export interface OptionsSplitView { /** - * Master view display mode + * Master view display mode. + * The following options will only work on iOS 14+: twoBesideSecondary, twoDisplaceSecondary, twoOverSecondary * @default 'auto' */ - displayMode?: 'auto' | 'visible' | 'hidden' | 'overlay'; + displayMode?: + | 'auto' + | 'visible' + | 'hidden' + | 'overlay' + | 'secondaryOnly' + | 'oneBesideSecondary' + | 'oneOverSecondary' + | 'twoBesideSecondary' // iOS 14+ only + | 'twoDisplaceSecondary' // iOS 14+ only + | 'twoOverSecondary'; // iOS 14+ only /** * Master view side. Leading is left. Trailing is right. * @default 'leading' diff --git a/website/docs/api/layout-splitView.mdx b/website/docs/api/layout-splitView.mdx index de1709b485d..d6a0c9f5be9 100644 --- a/website/docs/api/layout-splitView.mdx +++ b/website/docs/api/layout-splitView.mdx @@ -7,6 +7,12 @@ sidebar_label: SplitView A container view controller implementing a master-detail interface. See [UISplitViewController docs](https://developer.apple.com/documentation/uikit/uisplitviewcontroller). Currently implemented only in iOS. +UISplitViewController was extensively rewritten as part of iOS 14. This library provides two different APIs for SplitView, to support either the classic (pre-iOS 14) style or a "modern" column-based one (iOS 14+). + +You can still use either one on iOS 14, but some features such as triple column layouts are restricted to the newer API. + +# Classic API + ```js { id: 'PROFILE_TAB', @@ -48,3 +54,107 @@ Currently implemented only in iOS. | Type | Required | Description | | ----------------------- | -------- | ----------------------------------------------- | | [Options](options-root.mdx) | No | dynamic options which will apply to all screens | + +--- + +# Modern (iOS 14+ only) API + +Please note that in order to properly enable the new column-based layouts, you will also need to set a corresponding [displayMode](options-splitView.mdx) in the options. + +This should be one of the following values: + - "secondaryOnly" + - "oneBesideSecondary" + - "oneOverSecondary" + - "twoBesideSecondary" + - "twoDisplaceSecondary" + - "twoOverSecondary" + +To understand what these options do, refer the [DisplayMode docs](https://developer.apple.com/documentation/uikit/uisplitviewcontroller/displaymode). + +Other `displayMode` options will also work, but will fall back to creating a classic master-detail layout. + +Only setting `primary` and `secondary` will create a two-column layout. + +Additionally setting `supplementary` will automatically create a three-column layout. + +```js +{ + id: 'PROFILE_TAB', + primary: { + component: { + id: 'PRIMARY_SCREEN', + name: 'PrimaryScreen', + options: { + topBar: { + title: { + text: 'Primary', + }, + }, + }, + } + }, + supplementary: { + component: { + id: 'SUPPLEMENTARY_SCREEN', + name: 'SupplementaryScreen' + options: { + topBar: { + title: { + text: 'Supplementary', + }, + }, + }, + } + }, + secondary: { + component: { + id: 'SECONDARY_SCREEN', + name: 'SecondaryScreen' + options: { + topBar: { + title: { + text: 'Secondary', + }, + }, + }, + } + }, + options: { + displayMode: 'twoBesideSecondary' + } +} +``` + +It is also worth noting that each `component` is automatially embedded into a UINavigationView by iOS when using a column-based layout style. Therefore you can set `topBar` options such as `title` as shown above. + +A toggle icon is also displayed in the Navigation bar to hide / show the Primary component. + +## `id` + +| Type | Required | Description | +| ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string | No | Unique id used to interact with the view via the Navigation api, usually `Navigation.mergeOptions` which accepts the componentId as it's first argument. | + +## `primary` + +| Type | Required | Description | +| ------------------ | -------- | ----------------------------------------------- | +| [Layout](layout-layout.mdx) | YES | Set primary layout | + +## `supplementary` + +| Type | Required | Description | +| ------------------ | -------- | ----------------------------------------------- | +| [Layout](layout-layout.mdx) | NO | Set supplementary layout (for 3 column layouts on iOS 14+) | + +## `secondary` + +| Type | Required | Description | +| ------------------ | -------- | --------------------------------------------- | +| [Layout](layout-layout.mdx) | YES | Set secondary layout | + +## `options` + +| Type | Required | Description | +| ----------------------- | -------- | ----------------------------------------------- | +| [Options](options-root.mdx) | No | dynamic options which will apply to all screens | diff --git a/website/docs/api/options-splitView.mdx b/website/docs/api/options-splitView.mdx index 85f43f41e43..894e3ef95de 100644 --- a/website/docs/api/options-splitView.mdx +++ b/website/docs/api/options-splitView.mdx @@ -8,9 +8,19 @@ sidebar_label: SplitView Master view display mode. -| Type | Required | Default | Platform | -| -------------------------------------------- | -------- | ------- | -------- | -| enum('auto', 'visible', 'hidden', 'overlay') | No | 'auto' | iOS | +| Type | Required | Default | Platform | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- | -------- | +| enum('auto', 'visible', 'hidden', 'overlay', 'secondaryOnly', 'oneBesideSecondary', 'oneOverSecondary, 'twoBesideSecondary, 'twoDisplaceSecondary, 'twoOverSecondary') | No | 'auto' | iOS | + +This following values are only supported on iOS 14+: + - "secondaryOnly" + - "oneBesideSecondary" + - "oneOverSecondary" + - "twoBesideSecondary" + - "twoDisplaceSecondary" + - "twoOverSecondary" + +To understand what these options do, refer the [DisplayMode docs](https://developer.apple.com/documentation/uikit/uisplitviewcontroller/displaymode). ### `primaryEdge`