Skip to content

Commit

Permalink
adds support for column-based SplitViews on iOS 14+
Browse files Browse the repository at this point in the history
  • Loading branch information
jpdriver committed Nov 7, 2020
1 parent 01023b9 commit 27afc4d
Show file tree
Hide file tree
Showing 25 changed files with 291 additions and 73 deletions.
2 changes: 1 addition & 1 deletion e2e/SplitView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe(':ios: SplitView', () => {
});

it('push screen to master screen', async () => {
await elementById(TestIDs.PUSH_MASTER_BTN).tap();
await elementById(TestIDs.PUSH_PRIMARY_BTN).tap();
await expect(elementByLabel('Pushed Screen')).toBeVisible();
});

Expand Down
38 changes: 36 additions & 2 deletions lib/ios/RNNSplitViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,44 @@

@implementation RNNSplitViewController

- (instancetype)initWithLayoutInfo:(RNNLayoutInfo *)layoutInfo
creator:(id<RNNComponentViewCreator>)creator
options:(RNNNavigationOptions *)options
defaultOptions:(RNNNavigationOptions *)defaultOptions
presenter:(RNNBasePresenter *)presenter
eventEmitter:(RNNEventEmitter *)eventEmitter
childViewControllers:(NSArray *)childViewControllers {
if (@available(iOS 14.0, *)) {
NSString* style = options.splitView.style;
if ([style isEqualToString:@"tripleColumn"]) {
self = [self initWithStyle:UISplitViewControllerStyleTripleColumn];
} else if ([style isEqualToString:@"doubleColumn"]) {
self = [self initWithStyle:UISplitViewControllerStyleDoubleColumn];
} else {
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 loadChildren:childViewControllers];
[self.presenter bindViewController:self];
self.extendedLayoutIncludesOpaqueBars = YES;
[self.presenter applyOptionsOnInit:self.resolveOptions];

return self;
}

- (void)setViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers {
[super setViewControllers:viewControllers];
UIViewController<UISplitViewControllerDelegate> *masterViewController = viewControllers[0];
self.delegate = masterViewController;
UIViewController<UISplitViewControllerDelegate> *primaryViewController = viewControllers[0];
self.delegate = primaryViewController;
}

- (UIViewController *)getCurrentChild {
Expand Down
1 change: 1 addition & 0 deletions lib/ios/RNNSplitViewOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
@property(nonatomic, strong) Number *minWidth;
@property(nonatomic, strong) Number *maxWidth;
@property(nonatomic, strong) NSString *primaryBackgroundStyle;
@property(nonatomic, strong) NSString *style;

@end
1 change: 1 addition & 0 deletions lib/ios/RNNSplitViewOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
self.minWidth = [NumberParser parse:dict key:@"minWidth"];
self.maxWidth = [NumberParser parse:dict key:@"maxWidth"];
self.primaryBackgroundStyle = dict[@"primaryBackgroundStyle"];
self.style = dict[@"style"];
return self;
}

Expand Down
19 changes: 19 additions & 0 deletions lib/ios/UISplitViewController+RNNOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,30 @@ @implementation UISplitViewController (RNNOptions)

- (void)rnn_setDisplayMode:(NSString *)displayMode {
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;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class NavigationRoot {
this.componentWrapper,
appRegistryService
);
this.layoutTreeParser = new LayoutTreeParser(this.uniqueIdProvider);
this.layoutTreeParser = new LayoutTreeParser(this.uniqueIdProvider, new Deprecations());
const optionsProcessor = new OptionsProcessor(
this.store,
this.uniqueIdProvider,
Expand Down
3 changes: 2 additions & 1 deletion lib/src/commands/Commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LayoutTreeParser } from './LayoutTreeParser';
import { LayoutTreeCrawler } from './LayoutTreeCrawler';
import { Store } from '../components/Store';
import { Commands } from './Commands';
import { Deprecations } from './Deprecations';
import { CommandsObserver } from '../events/CommandsObserver';
import { NativeCommandsSender } from '../adapters/NativeCommandsSender';
import { OptionsProcessor } from './OptionsProcessor';
Expand Down Expand Up @@ -42,7 +43,7 @@ describe('Commands', () => {
uut = new Commands(
mockedStore,
instance(mockedNativeCommandsSender),
new LayoutTreeParser(uniqueIdProvider),
new LayoutTreeParser(uniqueIdProvider, new Deprecations()),
new LayoutTreeCrawler(instance(mockedStore), optionsProcessor),
commandsObserver,
uniqueIdProvider,
Expand Down
18 changes: 18 additions & 0 deletions lib/src/commands/Deprecations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import once from 'lodash/once';
import get from 'lodash/get';
import each from 'lodash/each';
import { Platform } from 'react-native';
import { Layout, LayoutSplitView } from 'react-native-navigation/interfaces/Layout';

export class Deprecations {
private deprecatedOptions: Array<{ key: string; showWarning: any }> = [
Expand Down Expand Up @@ -77,6 +78,17 @@ export class Deprecations {
}
}

public onParseLayout(api: Layout) {
if (
api.splitView &&
Platform.OS === 'ios' &&
typeof api.splitView.master !== 'undefined' &&
typeof api.splitView.detail !== 'undefined'
) {
this.deprecateMasterDetailSplitView(api.splitView);
}
}

public onProcessDefaultOptions(_key: string, _parentOptions: Record<string, any>) {}

private deprecateSearchBarOptions = once((parentOptions: object) => {
Expand All @@ -97,4 +109,10 @@ export class Deprecations {
parentOptions
);
});
private deprecateMasterDetailSplitView = once((api: LayoutSplitView) => {
console.warn(
`using SplitView with master and detail is deprecated on iOS. For more information see https://github.com/wix/react-native-navigation/pull/6705`,
api
);
});
}
7 changes: 4 additions & 3 deletions lib/src/commands/LayoutTreeParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import keys from 'lodash/keys';
import { LayoutTreeParser } from './LayoutTreeParser';
import { LayoutType } from './LayoutType';
import { Deprecations } from './Deprecations';
import { Options } from '../interfaces/Options';
import { Layout } from '../interfaces/Layout';
import { UniqueIdProvider } from '../adapters/UniqueIdProvider';
Expand All @@ -13,7 +14,7 @@ describe('LayoutTreeParser', () => {
beforeEach(() => {
mockedUniqueIdProvider = mock(UniqueIdProvider);
when(mockedUniqueIdProvider.generate(anything())).thenReturn('myUniqueId');
uut = new LayoutTreeParser(instance(mockedUniqueIdProvider));
uut = new LayoutTreeParser(instance(mockedUniqueIdProvider), new Deprecations());
});

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

const splitView: Layout = {
splitView: {
master: {
primary: {
stack: {
children: [singleComponent],
options,
},
},
detail: stackWithTopBar,
secondary: stackWithTopBar,
options: optionsSplitView,
},
};
Expand Down
32 changes: 27 additions & 5 deletions lib/src/commands/LayoutTreeParser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LayoutType } from './LayoutType';
import { LayoutNode } from './LayoutTreeCrawler';
import { Deprecations } from './Deprecations';
import {
Layout,
LayoutTopTabs,
Expand All @@ -13,7 +14,7 @@ import {
import { UniqueIdProvider } from '../adapters/UniqueIdProvider';

export class LayoutTreeParser {
constructor(private uniqueIdProvider: UniqueIdProvider) {
constructor(private uniqueIdProvider: UniqueIdProvider, private deprecations: Deprecations) {
this.parse = this.parse.bind(this);
}

Expand All @@ -31,6 +32,10 @@ export class LayoutTreeParser {
} else if (api.externalComponent) {
return this.externalComponent(api.externalComponent);
} else if (api.splitView) {
if (api.splitView.master || api.splitView.detail) {
// Deprecated
this.deprecations.onParseLayout(api);
}
return this.splitView(api.splitView);
}
throw new Error(`unknown LayoutType "${Object.keys(api)}"`);
Expand Down Expand Up @@ -126,14 +131,31 @@ 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;

return {
id: api.id || this.uniqueIdProvider.generate(LayoutType.SplitView),
type: LayoutType.SplitView,
data: { options: api.options },
children: master && detail ? [master, detail] : [],
children: this.splitViewChildren(api),
};
}

private splitViewChildren(api: LayoutSplitView): LayoutNode[] {
const children: LayoutNode[] = [];
if (api.primary) {
children.push(this.parse(api.primary));
} else if (api.master) {
// Deprecated -- treat as `primary`
children.push(this.parse(api.master));
}
if (api.supplementary) {
children.push(this.parse(api.supplementary));
}
if (api.secondary) {
children.push(this.parse(api.secondary));
} else if (api.detail) {
// Deprecated -- treat as `secondary`
children.push(this.parse(api.detail));
}
return children;
}
}
27 changes: 22 additions & 5 deletions lib/src/interfaces/Layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,26 +105,43 @@ export interface LayoutSideMenu {
options?: Options;
}

export interface LayoutSplitView {
export interface LayoutSplitViewCurrent {
/**
* Set ID of the stack so you can use Navigation.mergeOptions to
* update options
*/
id?: string;
/**
* Set master layout (the smaller screen, sidebar)
* Set primary layout
*/
master?: Layout;
primary?: Layout;
/**
* Set detail layout (the larger screen, flexes)
* Set supplementary layout (for 3 column layouts on iOS 14+)
*/
detail?: Layout;
supplementary?: Layout;
/**
* Set secondary layout
*/
secondary?: Layout;
/**
* Configure split view
*/
options?: Options;
}

export interface LayoutSplitViewDeprecated {
/**
* Set master layout (the smaller screen, sidebar)
*/
master?: Layout;
/**
* Set master layout (the smaller screen, sidebar)
*/
detail?: Layout;
}

export type LayoutSplitView = LayoutSplitViewCurrent & LayoutSplitViewDeprecated;

export interface LayoutTopTabs {
/**
* Set the layout's id so Navigation.mergeOptions can be used to update options
Expand Down
28 changes: 22 additions & 6 deletions lib/src/interfaces/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,44 @@ type Interpolation =

export interface OptionsSplitView {
/**
* Master view display mode
* Primary view display mode.
* The following options will only work on iOS 14+: twoBesideSecondary, twoDisplaceSecondary, twoOverSecondary
* @default 'auto'
*/
displayMode?: 'auto' | 'visible' | 'hidden' | 'overlay';
/**
* Master view side. Leading is left. Trailing is right.
displayMode?:
| 'auto'
| 'visible'
| 'hidden'
| 'overlay'
| 'secondaryOnly'
| 'oneBesideSecondary'
| 'oneOverSecondary'
| 'twoBesideSecondary' // iOS 14+ only
| 'twoDisplaceSecondary' // iOS 14+ only
| 'twoOverSecondary'; // iOS 14+ only
/**
* Primary view side. Leading is left. Trailing is right.
* @default 'leading'
*/
primaryEdge?: 'leading' | 'trailing';
/**
* Set the minimum width of master view
* Set the minimum width of primary view
*/
minWidth?: number;
/**
* Set the maximum width of master view
* Set the maximum width of primary view
*/
maxWidth?: number;
/**
* Set background style of sidebar. Currently works for Mac Catalyst apps only.
* @default 'none'
*/
primaryBackgroundStyle?: 'none' | 'sidebar';
/**
* Describe the number of columns the split view interface displays (iOS 14+)
* @default 'unspecified'
*/
style?: 'unspecified' | 'doubleColumn' | 'tripleColumn';
}

export interface OptionsStatusBar {
Expand Down
11 changes: 7 additions & 4 deletions playground/src/screens/LayoutsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,24 +113,24 @@ export default class LayoutsScreen extends NavigationComponent {
id: 'SPLITVIEW_ID',
master: {
stack: {
id: 'MASTER_ID',
id: 'PRIMARY_ID',
children: [
{
component: {
name: Screens.CocktailsListMasterScreen,
name: Screens.CocktailsListPrimaryScreen,
},
},
],
},
},
detail: {
stack: {
id: 'DETAILS_ID',
id: 'SECONDARY_ID',
children: [
{
component: {
id: 'DETAILS_COMPONENT_ID',
name: Screens.CocktailDetailsScreen,
name: Screens.CocktailSecondaryScreen,
},
},
],
Expand All @@ -142,6 +142,9 @@ export default class LayoutsScreen extends NavigationComponent {
},
splitView: {
displayMode: 'visible',
minWidth: 375,
maxWidth: 375,
style: 'doubleColumn',
},
},
},
Expand Down
Loading

0 comments on commit 27afc4d

Please sign in to comment.