瀏覽代碼

[v2] UISplitViewController (#3247)

* Work in progress

* Arbitary changes

* Improvements on API

* Cleanup RNNSplitViewController, allow mergeOptions

* Added documentation and better playground example

* Adding tests for coverage

* Find out why this test is failing.

* Only show split view on iOS

* Add test back in
Birkir Rafn Guðjónsson 6 年之前
父節點
當前提交
07376e9e29

+ 24
- 0
docs/docs/layout-types.md 查看文件

@@ -79,4 +79,28 @@ const sideMenu = {
79 79
     component: {}
80 80
   }
81 81
 }
82
+```
83
+
84
+## splitView
85
+
86
+Master and Detail based layout.
87
+
88
+You can change the it's options with `Navigation.mergeOptions('splitView1', { maxWidth: 400 })`.
89
+
90
+```js
91
+const splitView = {
92
+  id: 'splitView1', // Required to update options
93
+  master: {
94
+    // All layout types accepted supported by device, eg. `stack`
95
+  },
96
+  detail: {
97
+    // All layout types accepted supported by device, eg. `stack`
98
+  },
99
+  options: {
100
+    displayMode: 'auto', // Master view display mode: `auto`, `visible`, `hidden` and `overlay`
101
+    primaryEdge: 'leading', // Master view side: `leading` or `trailing`
102
+    minWidth: 150, // Minimum width of master view
103
+    maxWidth: 300, // Maximum width of master view
104
+  },
105
+}
82 106
 ```

+ 13
- 0
lib/ios/RNNCommandsHandler.m 查看文件

@@ -4,6 +4,7 @@
4 4
 #import "RNNOverlayManager.h"
5 5
 #import "RNNNavigationOptions.h"
6 6
 #import "RNNRootViewController.h"
7
+#import "RNNSplitViewController.h"
7 8
 #import "React/RCTUIManager.h"
8 9
 
9 10
 static NSString* const setRoot	= @"setRoot";
@@ -70,6 +71,18 @@ static NSString* const setDefaultOptions	= @"setDefaultOptions";
70 71
 		
71 72
 		[CATransaction commit];
72 73
 	}
74
+
75
+	if ([vc isKindOfClass:[RNNSplitViewController class]]) {
76
+		RNNSplitViewController* splitVc = (RNNSplitViewController*)vc;
77
+		[splitVc.options mergeWith:options];
78
+		[CATransaction begin];
79
+		[CATransaction setCompletionBlock:completion];
80
+		
81
+		[splitVc.options applyOn:vc];
82
+		
83
+		[CATransaction commit];
84
+	}
85
+	
73 86
 }
74 87
 
75 88
 -(void) setDefaultOptions:(NSDictionary*)optionsDict completion:(RNNTransitionCompletionBlock)completion {

+ 30
- 0
lib/ios/RNNControllerFactory.m 查看文件

@@ -2,6 +2,8 @@
2 2
 #import "RNNControllerFactory.h"
3 3
 #import "RNNLayoutNode.h"
4 4
 #import "RNNRootViewController.h"
5
+#import "RNNSplitViewController.h"
6
+#import "RNNSplitViewOptions.h"
5 7
 #import "RNNSideMenuController.h"
6 8
 #import "RNNSideMenuChildVC.h"
7 9
 #import "RNNNavigationOptions.h"
@@ -79,6 +81,10 @@
79 81
 		result = [self createExternalComponent:node];
80 82
 	}
81 83
 	
84
+	else if (node.isSplitView) {
85
+		result = [self createSplitView:node];
86
+	}
87
+	
82 88
 	if (!result) {
83 89
 		@throw [NSException exceptionWithName:@"UnknownControllerType" reason:[@"Unknown controller type " stringByAppendingString:node.type] userInfo:nil];
84 90
 	}
@@ -204,4 +210,28 @@
204 210
 	return vc;
205 211
 }
206 212
 
213
+- (UIViewController<RNNRootViewProtocol> *)createSplitView:(RNNLayoutNode*)node {
214
+
215
+	NSString* componentId = node.nodeId;
216
+	
217
+	RNNSplitViewOptions* options = [[RNNSplitViewOptions alloc] initWithDict:_defaultOptionsDict];
218
+	[options mergeWith:node.data[@"options"]];
219
+
220
+	RNNSplitViewController* svc = [[RNNSplitViewController alloc] initWithOptions:options withComponentId:componentId rootViewCreator:_creator eventEmitter:_eventEmitter];
221
+
222
+	// We need two children of the node for successful Master / Detail
223
+	NSDictionary *master = node.children[0];
224
+	NSDictionary *detail = node.children[1];
225
+
226
+	// Create view controllers
227
+	RNNRootViewController* masterVc = (RNNRootViewController*)[self fromTree:master];
228
+	RNNRootViewController* detailVc = (RNNRootViewController*)[self fromTree:detail];
229
+
230
+	// Set the controllers and delegate to masterVC
231
+	svc.viewControllers = [NSArray arrayWithObjects:masterVc, detailVc, nil];
232
+	svc.delegate = masterVc;
233
+
234
+	return svc;
235
+}
236
+
207 237
 @end

+ 2
- 0
lib/ios/RNNLayoutNode.h 查看文件

@@ -8,6 +8,7 @@
8 8
 @property NSString* nodeId;
9 9
 @property NSDictionary* data;
10 10
 @property NSArray* children;
11
+@property NSArray* sidebar;
11 12
 
12 13
 +(instancetype)create:(NSDictionary *)json;
13 14
 
@@ -20,5 +21,6 @@
20 21
 -(BOOL)isSideMenuLeft;
21 22
 -(BOOL)isSideMenuRight;
22 23
 -(BOOL)isSideMenuCenter;
24
+-(BOOL)isSplitView;
23 25
 
24 26
 @end

+ 5
- 0
lib/ios/RNNLayoutNode.m 查看文件

@@ -10,6 +10,7 @@
10 10
 	node.nodeId = json[@"id"];
11 11
 	node.data = json[@"data"];
12 12
 	node.children = json[@"children"];
13
+	
13 14
 	return node;
14 15
 }
15 16
 
@@ -49,5 +50,9 @@
49 50
 {
50 51
 	return [self.type isEqualToString:@"SideMenuCenter"];
51 52
 }
53
+-(BOOL)isSplitView
54
+{
55
+	return [self.type isEqualToString:@"SplitView"];
56
+}
52 57
 
53 58
 @end

+ 1
- 0
lib/ios/RNNNavigationOptions.m 查看文件

@@ -5,6 +5,7 @@
5 5
 #import "RNNTopBarOptions.h"
6 6
 #import "RNNSideMenuController.h"
7 7
 #import "RNNRootViewController.h"
8
+#import "RNNSplitViewController.h"
8 9
 #import "RNNNavigationButtons.h"
9 10
 
10 11
 const NSInteger BLUR_STATUS_TAG = 78264801;

+ 1
- 1
lib/ios/RNNRootViewProtocol.h 查看文件

@@ -1,6 +1,6 @@
1 1
 #import "RNNNavigationOptions.h"
2 2
 
3
-@protocol RNNRootViewProtocol <NSObject, UINavigationControllerDelegate, UIViewControllerTransitioningDelegate>
3
+@protocol RNNRootViewProtocol <NSObject, UINavigationControllerDelegate, UIViewControllerTransitioningDelegate, UISplitViewControllerDelegate>
4 4
 
5 5
 @optional
6 6
 - (void)mergeOptions:(NSDictionary*)options;

+ 27
- 0
lib/ios/RNNSplitViewController.h 查看文件

@@ -0,0 +1,27 @@
1
+
2
+#import <Foundation/Foundation.h>
3
+#import <UIKit/UIKit.h>
4
+#import "RNNLayoutNode.h"
5
+#import "RNNRootViewCreator.h"
6
+#import "RNNEventEmitter.h"
7
+#import "RNNNavigationOptions.h"
8
+#import "RNNSplitViewOptions.h"
9
+#import "RNNAnimator.h"
10
+#import "RNNTopTabsViewController.h"
11
+#import "RNNRootViewProtocol.h"
12
+
13
+@interface RNNSplitViewController : UISplitViewController	<RNNRootViewProtocol>
14
+
15
+@property (nonatomic, strong) RNNSplitViewOptions* options;
16
+@property (nonatomic, strong) RNNEventEmitter *eventEmitter;
17
+@property (nonatomic, strong) NSString* componentId;
18
+@property (nonatomic) id<RNNRootViewCreator> creator;
19
+
20
+-(instancetype)initWithOptions:(RNNSplitViewOptions*)options
21
+			withComponentId:(NSString*)componentId
22
+			rootViewCreator:(id<RNNRootViewCreator>)creator
23
+				  eventEmitter:(RNNEventEmitter*)eventEmitter;
24
+
25
+-(BOOL)isCustomTransitioned;
26
+
27
+@end

+ 44
- 0
lib/ios/RNNSplitViewController.m 查看文件

@@ -0,0 +1,44 @@
1
+
2
+#import "RNNRootViewController.h"
3
+#import "RNNSplitViewController.h"
4
+
5
+@interface RNNSplitViewController()
6
+@property (nonatomic) BOOL _optionsApplied;
7
+@property (nonatomic, copy) void (^rotationBlock)(void);
8
+@end
9
+
10
+@implementation RNNSplitViewController
11
+
12
+-(instancetype)initWithOptions:(RNNSplitViewOptions*)options
13
+			withComponentId:(NSString*)componentId
14
+			rootViewCreator:(id<RNNRootViewCreator>)creator
15
+			   eventEmitter:(RNNEventEmitter*)eventEmitter {
16
+	self = [super init];
17
+	self.componentId = componentId;
18
+	self.options = options;
19
+	self.eventEmitter = eventEmitter;
20
+	self.creator = creator;
21
+
22
+	self.navigationController.delegate = self;
23
+
24
+	return self;
25
+}
26
+
27
+-(void)viewWillAppear:(BOOL)animated{
28
+	[super viewWillAppear:animated];
29
+	[self.options applyOn:self];
30
+	[self optionsUpdated];
31
+}
32
+
33
+- (void)optionsUpdated {
34
+}
35
+
36
+- (void)mergeOptions:(NSDictionary *)options {
37
+	[self.options mergeIfEmptyWith:options];
38
+}
39
+
40
+-(BOOL)isCustomTransitioned {
41
+	return false;
42
+}
43
+
44
+@end

+ 10
- 0
lib/ios/RNNSplitViewOptions.h 查看文件

@@ -0,0 +1,10 @@
1
+#import "RNNOptions.h"
2
+
3
+@interface RNNSplitViewOptions : RNNOptions
4
+
5
+@property (nonatomic, strong) NSString* displayMode;
6
+@property (nonatomic, strong) NSString* primaryEdge;
7
+@property (nonatomic, strong) NSNumber* minWidth;
8
+@property (nonatomic, strong) NSNumber* maxWidth;
9
+
10
+@end

+ 37
- 0
lib/ios/RNNSplitViewOptions.m 查看文件

@@ -0,0 +1,37 @@
1
+#import "RNNSplitViewOptions.h"
2
+#import "RNNRootViewProtocol.h"
3
+
4
+@implementation RNNSplitViewOptions
5
+
6
+-(void)applyOn:(UIViewController<RNNRootViewProtocol> *)viewController {
7
+	
8
+	UISplitViewController *svc = (UISplitViewController*) viewController;
9
+
10
+	if (@available(iOS 11.0, *)) {
11
+		if ([self.primaryEdge isEqualToString:@"trailing"]) {
12
+			[svc setPrimaryEdge:UISplitViewControllerPrimaryEdgeTrailing];
13
+		} else {
14
+			[svc setPrimaryEdge:UISplitViewControllerPrimaryEdgeLeading];
15
+		}
16
+	}
17
+	
18
+	if ([self.displayMode isEqualToString:@"visible"]) {
19
+		[svc setPreferredDisplayMode:UISplitViewControllerDisplayModeAllVisible];
20
+	} else if ([self.displayMode isEqualToString:@"hidden"]) {
21
+		[svc setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
22
+	} else if ([self.displayMode isEqualToString:@"overlay"]) {
23
+		[svc setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryOverlay];
24
+	} else {
25
+		[svc setPreferredDisplayMode:UISplitViewControllerDisplayModeAutomatic];
26
+	}
27
+
28
+	if (self.minWidth) {
29
+		[svc setMinimumPrimaryColumnWidth:[self.minWidth doubleValue]];
30
+	}
31
+
32
+	if (self.maxWidth) {
33
+		[svc setMaximumPrimaryColumnWidth:[self.maxWidth doubleValue]];
34
+	}
35
+}
36
+
37
+@end

+ 19
- 0
lib/ios/ReactNativeNavigation.xcodeproj/project.pbxproj 查看文件

@@ -153,6 +153,10 @@
153 153
 		7BEF0D1D1E43771B003E96B0 /* RNNLayoutNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BEF0D1B1E43771B003E96B0 /* RNNLayoutNode.m */; };
154 154
 		A7626BFD1FC2FB2C00492FB8 /* RNNTopBarOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = A7626BFC1FC2FB2C00492FB8 /* RNNTopBarOptions.m */; };
155 155
 		A7626C011FC5796200492FB8 /* RNNBottomTabsOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = A7626C001FC5796200492FB8 /* RNNBottomTabsOptions.m */; };
156
+		E33AC20020B5BA0B0090DB8A /* RNNSplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E33AC1FF20B5BA0B0090DB8A /* RNNSplitViewController.m */; };
157
+		E33AC20420B5C3890090DB8A /* RNNStatusBarOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = E33AC20220B5C3880090DB8A /* RNNStatusBarOptions.h */; };
158
+		E33AC20520B5C3890090DB8A /* RNNStatusBarOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = E33AC20320B5C3890090DB8A /* RNNStatusBarOptions.m */; };
159
+		E33AC20820B5C4F90090DB8A /* RNNSplitViewOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = E33AC20720B5C4F90090DB8A /* RNNSplitViewOptions.m */; };
156 160
 		E8367B801F7A8A4700675C05 /* VICMAImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = E8367B7E1F7A8A4700675C05 /* VICMAImageView.h */; };
157 161
 		E8367B811F7A8A4700675C05 /* VICMAImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = E8367B7F1F7A8A4700675C05 /* VICMAImageView.m */; };
158 162
 		E83BAD681F2734B500A9F3DD /* RNNNavigationOptionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E83BAD671F2734B500A9F3DD /* RNNNavigationOptionsTest.m */; };
@@ -355,6 +359,12 @@
355 359
 		A7626BFF1FC578AB00492FB8 /* RNNBottomTabsOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNNBottomTabsOptions.h; sourceTree = "<group>"; };
356 360
 		A7626C001FC5796200492FB8 /* RNNBottomTabsOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNBottomTabsOptions.m; sourceTree = "<group>"; };
357 361
 		D8AFADBD1BEE6F3F00A4592D /* libReactNativeNavigation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactNativeNavigation.a; sourceTree = BUILT_PRODUCTS_DIR; };
362
+		E33AC1FF20B5BA0B0090DB8A /* RNNSplitViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNSplitViewController.m; sourceTree = "<group>"; };
363
+		E33AC20120B5BA550090DB8A /* RNNSplitViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNNSplitViewController.h; sourceTree = "<group>"; };
364
+		E33AC20220B5C3880090DB8A /* RNNStatusBarOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNNStatusBarOptions.h; sourceTree = "<group>"; };
365
+		E33AC20320B5C3890090DB8A /* RNNStatusBarOptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNStatusBarOptions.m; sourceTree = "<group>"; };
366
+		E33AC20620B5C49E0090DB8A /* RNNSplitViewOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNNSplitViewOptions.h; sourceTree = "<group>"; };
367
+		E33AC20720B5C4F90090DB8A /* RNNSplitViewOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNSplitViewOptions.m; sourceTree = "<group>"; };
358 368
 		E8367B7E1F7A8A4700675C05 /* VICMAImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VICMAImageView.h; sourceTree = "<group>"; };
359 369
 		E8367B7F1F7A8A4700675C05 /* VICMAImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VICMAImageView.m; sourceTree = "<group>"; };
360 370
 		E83BAD671F2734B500A9F3DD /* RNNNavigationOptionsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNNavigationOptionsTest.m; sourceTree = "<group>"; };
@@ -501,6 +511,8 @@
501 511
 		504AFE611FFE52EF0076E904 /* Options */ = {
502 512
 			isa = PBXGroup;
503 513
 			children = (
514
+				E33AC20220B5C3880090DB8A /* RNNStatusBarOptions.h */,
515
+				E33AC20320B5C3890090DB8A /* RNNStatusBarOptions.m */,
504 516
 				504AFE621FFE53070076E904 /* RNNOptions.h */,
505 517
 				504AFE631FFE53070076E904 /* RNNOptions.m */,
506 518
 				E83BAD691F27362500A9F3DD /* RNNNavigationOptions.h */,
@@ -537,6 +549,8 @@
537 549
 				50451D082042E20600695F00 /* RNNTransitionsOptions.m */,
538 550
 				50415CB820553B8E00BB682E /* RNNScreenTransition.h */,
539 551
 				50415CB920553B8E00BB682E /* RNNScreenTransition.m */,
552
+				E33AC20620B5C49E0090DB8A /* RNNSplitViewOptions.h */,
553
+				E33AC20720B5C4F90090DB8A /* RNNSplitViewOptions.m */,
540 554
 			);
541 555
 			name = Options;
542 556
 			sourceTree = "<group>";
@@ -600,6 +614,8 @@
600 614
 				507F43F61FF525B500D9425B /* RNNSegmentedControl.h */,
601 615
 				507F43F71FF525B500D9425B /* RNNSegmentedControl.m */,
602 616
 				507F441F1FFA8A8800D9425B /* RNNRootViewProtocol.h */,
617
+				E33AC1FF20B5BA0B0090DB8A /* RNNSplitViewController.m */,
618
+				E33AC20120B5BA550090DB8A /* RNNSplitViewController.h */,
603 619
 			);
604 620
 			name = Controllers;
605 621
 			sourceTree = "<group>";
@@ -950,6 +966,7 @@
950 966
 				7BA500751E2544B9001B9E1B /* ReactNativeNavigation.m in Sources */,
951 967
 				263905B21E4C6F440023D7D3 /* MMDrawerController.m in Sources */,
952 968
 				E8AEDB3D1F55A1C2000F5A6A /* RNNElementView.m in Sources */,
969
+				E33AC20520B5C3890090DB8A /* RNNStatusBarOptions.m in Sources */,
953 970
 				E83BAD6B1F27363A00A9F3DD /* RNNNavigationOptions.m in Sources */,
954 971
 				506A2B1520973DFD00F43A95 /* RNNErrorHandler.m in Sources */,
955 972
 				7BBFE5441E25330E002A6182 /* RNNBridgeModule.m in Sources */,
@@ -974,6 +991,8 @@
974 991
 				214545251F4DC125006E8DA1 /* RNNUIBarButtonItem.m in Sources */,
975 992
 				263905B81E4C6F440023D7D3 /* UIViewController+MMDrawerController.m in Sources */,
976 993
 				263905CD1E4C6F440023D7D3 /* SidebarWunderlistAnimation.m in Sources */,
994
+				E33AC20820B5C4F90090DB8A /* RNNSplitViewOptions.m in Sources */,
995
+				E33AC20020B5BA0B0090DB8A /* RNNSplitViewController.m in Sources */,
977 996
 				263905CF1E4C6F440023D7D3 /* TheSidebarController.m in Sources */,
978 997
 				E8A430121F9CB87B00B61A20 /* RNNAnimatedView.m in Sources */,
979 998
 				507F43F51FF4FCFE00D9425B /* HMSegmentedControl.m in Sources */,

+ 35
- 1
lib/src/commands/LayoutTreeParser.test.ts 查看文件

@@ -126,6 +126,16 @@ describe('LayoutTreeParser', () => {
126 126
       expect(result.children[1].children[0].children[2].children[0].children[2].children[4].type).toEqual('Stack');
127 127
       expect(result.children[1].children[0].children[2].children[0].children[2].data).toEqual({ options: { topBar: { title: { text: 'Hello1'} } } });
128 128
     });
129
+
130
+    it('split view', () => {
131
+      const result = uut.parse(LayoutExamples.splitView);
132
+      const master = uut.parse(LayoutExamples.splitView.splitView.master);
133
+      const detail = uut.parse(LayoutExamples.splitView.splitView.detail);
134
+
135
+      expect(result.type).toEqual('SplitView');
136
+      expect(result.children[0]).toEqual(master);
137
+      expect(result.children[1]).toEqual(detail);
138
+    });
129 139
   });
130 140
 
131 141
   it('options for all containing types', () => {
@@ -134,6 +144,7 @@ describe('LayoutTreeParser', () => {
134 144
     expect(uut.parse({ bottomTabs: { options } }).data.options).toBe(options);
135 145
     expect(uut.parse({ topTabs: { options } }).data.options).toBe(options);
136 146
     expect(uut.parse({ sideMenu: { options, center: { component: {} } } }).data.options).toBe(options);
147
+    expect(uut.parse(LayoutExamples.splitView).data.options).toBe(optionsSplitView);
137 148
   });
138 149
 
139 150
   it('pass user provided id as is', () => {
@@ -169,6 +180,13 @@ const options = {
169 180
   }
170 181
 };
171 182
 
183
+const optionsSplitView = {
184
+  displayMode: 'auto',
185
+  primaryEdge: 'leading',
186
+  minWidth: 150,
187
+  maxWidth: 300,
188
+};
189
+
172 190
 const singleComponent = {
173 191
   component: {
174 192
     name: 'MyReactComponent',
@@ -279,6 +297,21 @@ const complexLayout = {
279 297
   }
280 298
 };
281 299
 
300
+const splitView = {
301
+  splitView: {
302
+    master: {
303
+      stack: {
304
+        children: [
305
+          singleComponent,
306
+        ],
307
+        options
308
+      },
309
+    },
310
+    detail: stackWithTopBar,
311
+    options: optionsSplitView,
312
+  },
313
+};
314
+
282 315
 const LayoutExamples = {
283 316
   passProps,
284 317
   options,
@@ -288,5 +321,6 @@ const LayoutExamples = {
288 321
   sideMenu,
289 322
   topTabs,
290 323
   complexLayout,
291
-  externalComponent
324
+  externalComponent,
325
+  splitView
292 326
 };

+ 17
- 0
lib/src/commands/LayoutTreeParser.ts 查看文件

@@ -20,6 +20,8 @@ export class LayoutTreeParser {
20 20
       return this._component(api.component);
21 21
     } else if (api.externalComponent) {
22 22
       return this._externalComponent(api.externalComponent);
23
+    } else if (api.splitView) {
24
+      return this._splitView(api.splitView);
23 25
     }
24 26
     throw new Error(`unknown LayoutType "${_.keys(api)}"`);
25 27
   }
@@ -107,4 +109,19 @@ export class LayoutTreeParser {
107 109
       children: []
108 110
     };
109 111
   }
112
+
113
+  _splitView(api): LayoutNode {
114
+    const master = this.parse(api.master);
115
+    const detail = this.parse(api.detail);
116
+
117
+    return {
118
+      id: api.id,
119
+      type: LayoutType.SplitView,
120
+      data: { name: api.name, options: api.options },
121
+      children: [
122
+        master,
123
+        detail,
124
+      ],
125
+    };
126
+  }
110 127
 }

+ 2
- 1
lib/src/commands/LayoutType.ts 查看文件

@@ -7,5 +7,6 @@ export enum LayoutType {
7 7
   SideMenuLeft = 'SideMenuLeft',
8 8
   SideMenuRight = 'SideMenuRight',
9 9
   TopTabs = 'TopTabs',
10
-  ExternalComponent = 'ExternalComponent'
10
+  ExternalComponent = 'ExternalComponent',
11
+  SplitView = 'SplitView'
11 12
 }

+ 9
- 0
playground/ios/playground.xcodeproj/project.pbxproj 查看文件

@@ -233,6 +233,13 @@
233 233
 			remoteGlobalIDString = 2D2A28881D9B049200D4039D;
234 234
 			remoteInfo = "RCTWebSocket-tvOS";
235 235
 		};
236
+		E33AC1FB20B5AFC70090DB8A /* PBXContainerItemProxy */ = {
237
+			isa = PBXContainerItemProxy;
238
+			containerPortal = 7B8F2FCA1E840F3600110AEC /* React.xcodeproj */;
239
+			proxyType = 2;
240
+			remoteGlobalIDString = 9936F32F1F5F2E5B0010BF04;
241
+			remoteInfo = "privatedata-tvOS";
242
+		};
236 243
 		E506412420A3138100EC769F /* PBXContainerItemProxy */ = {
237 244
 			isa = PBXContainerItemProxy;
238 245
 			containerPortal = 7B8F2FCA1E840F3600110AEC /* React.xcodeproj */;
@@ -907,6 +914,7 @@
907 914
 				PRODUCT_BUNDLE_IDENTIFIER = com.reactnativenavigation.playground;
908 915
 				PRODUCT_NAME = playground;
909 916
 				PROVISIONING_PROFILE_SPECIFIER = "";
917
+				TARGETED_DEVICE_FAMILY = "1,2";
910 918
 			};
911 919
 			name = Debug;
912 920
 		};
@@ -926,6 +934,7 @@
926 934
 				PRODUCT_BUNDLE_IDENTIFIER = com.reactnativenavigation.playground;
927 935
 				PRODUCT_NAME = playground;
928 936
 				PROVISIONING_PROFILE_SPECIFIER = "";
937
+				TARGETED_DEVICE_FAMILY = "1,2";
929 938
 			};
930 939
 			name = Release;
931 940
 		};

+ 41
- 0
playground/src/screens/WelcomeScreen.js 查看文件

@@ -34,6 +34,7 @@ class WelcomeScreen extends Component {
34 34
           <Text testID={testIDs.WELCOME_SCREEN_HEADER} style={styles.h1}>{`React Native Navigation!`}</Text>
35 35
           <Button title='Switch to tab based app' testID={testIDs.TAB_BASED_APP_BUTTON} onPress={this.onClickSwitchToTabs} />
36 36
           <Button title='Switch to app with side menus' testID={testIDs.TAB_BASED_APP_SIDE_BUTTON} onPress={this.onClickSwitchToSideMenus} />
37
+          {Platform.OS === 'ios' && <Button title='Switch to split view based app' testID={testIDs.SPLIT_VIEW_BUTTON} onPress={this.onClickSplitView} />}
37 38
           <Button title='Push Lifecycle Screen' testID={testIDs.PUSH_LIFECYCLE_BUTTON} onPress={this.onClickLifecycleScreen} />
38 39
           <Button title='Static Lifecycle Events' testID={testIDs.PUSH_STATIC_LIFECYCLE_BUTTON} onPress={this.onClickShowStaticLifecycleOverlay} />
39 40
           <Button title='Push' testID={testIDs.PUSH_BUTTON} onPress={this.onClickPush} />
@@ -447,6 +448,46 @@ class WelcomeScreen extends Component {
447 448
       }
448 449
     });
449 450
   }
451
+
452
+  onClickSplitView = () => {
453
+    Navigation.setRoot({
454
+      root: {
455
+        splitView: {
456
+          id: 'SPLITVIEW_ID',
457
+          master: {
458
+            stack: {
459
+              id: 'MASTER_ID',
460
+              children: [
461
+                {
462
+                  component: {
463
+                    name: 'navigation.playground.WelcomeScreen'
464
+                  },
465
+                },
466
+              ]
467
+            },
468
+          },
469
+          detail: {
470
+            stack: {
471
+              id: 'DETAILS_ID',
472
+              children: [
473
+                {
474
+                  component: {
475
+                    name: 'navigation.playground.WelcomeScreen'
476
+                  },
477
+                },
478
+              ]
479
+            }
480
+          },
481
+          options: {
482
+            displayMode: 'auto',
483
+            primaryEdge: 'leading',
484
+            minWidth: 150,
485
+            maxWidth: 300,
486
+          },
487
+        },
488
+      },
489
+    });
490
+  }
450 491
 }
451 492
 
452 493
 module.exports = WelcomeScreen;

+ 1
- 0
playground/src/testIDs.js 查看文件

@@ -60,6 +60,7 @@ module.exports = {
60 60
   EXTERNAL_COMPONENT_IN_DEEP_STACK: `EXTERNAL_COMPONENT_IN_DEEP_STACK`,
61 61
   SET_STACK_ROOT_BUTTON: `SET_STACK_ROOT_BUTTON`,
62 62
   MODAL_LIFECYCLE_BUTTON: `MODAL_LIFECYCLE_BUTTON`,
63
+  SPLIT_VIEW_BUTTON: `SPLIT_VIEW_BUTTON`,
63 64
 
64 65
   // Elements
65 66
   SCROLLVIEW_ELEMENT: `SCROLLVIEW_ELEMENT`,

+ 5378
- 0
yarn.lock
文件差異過大導致無法顯示
查看文件