Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] adds support for column-based SplitViews on iOS 14+ #6705

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
43 changes: 41 additions & 2 deletions lib/ios/RNNSplitViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,49 @@

@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 __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
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];
}
#else
self = [self init];
#endif

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
3 changes: 3 additions & 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 All @@ -24,6 +25,8 @@ - (void)mergeOptions:(RNNSplitViewOptions *)options {
self.maxWidth = options.maxWidth;
if (options.primaryBackgroundStyle)
self.primaryBackgroundStyle = options.primaryBackgroundStyle;
if (options.style)
self.style = options.style;
}

@end
25 changes: 24 additions & 1 deletion lib/ios/UISplitViewController+RNNOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,35 @@ @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 __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
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;
}
}
#endif
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 @@ -78,28 +78,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
Loading