瀏覽代碼

V2 custom controller (#2784)

* custom viewController support

* native component support and e2e

* custom viewController support

* native component support and e2e

* unit test fix
yogevbd 6 年之前
父節點
當前提交
eb9142a3e1
No account linked to committer's email address

+ 6
- 0
e2e/ScreenStack.test.js 查看文件

@@ -93,4 +93,10 @@ describe('screen stack', () => {
93 93
     await elementById(testIDs.POP_BUTTON).tap();
94 94
     await expect(elementByLabel('Screen 1')).toBeVisible();
95 95
   });
96
+
97
+  it(':ios: push native component with options', async () => {
98
+    await elementById(testIDs.PUSH_NATIVE_COMPONENT_BUTTON).tap();
99
+    await expect(elementById('TestLabel')).toBeVisible();
100
+    await expect(elementById(testIDs.TOP_BAR_ELEMENT)).toBeVisible();
101
+  });
96 102
 });

+ 13
- 7
lib/ios/RNNControllerFactory.m 查看文件

@@ -43,8 +43,8 @@
43 43
 	
44 44
 	UIViewController<RNNRootViewProtocol> *result;
45 45
 	
46
-	if ( node.isComponent) {
47
-		result = [self createComponent:node];
46
+	if (node.isComponent) {
47
+		result = [self createComponent:node nativeComponent:NO];
48 48
 	}
49 49
 	
50 50
 	else if (node.isStack)	{
@@ -70,10 +70,15 @@
70 70
 	else if (node.isSideMenuLeft) {
71 71
 		result = [self createSideMenuChild:node type:RNNSideMenuChildTypeLeft];
72 72
 	}
73
+	
73 74
 	else if (node.isSideMenuRight) {
74 75
 		result = [self createSideMenuChild:node type:RNNSideMenuChildTypeRight];
75 76
 	}
76 77
 	
78
+	else if ( node.isNativeComponent) {
79
+		result = [self createComponent:node nativeComponent:YES];
80
+	}
81
+	
77 82
 	if (!result) {
78 83
 		@throw [NSException exceptionWithName:@"UnknownControllerType" reason:[@"Unknown controller type " stringByAppendingString:node.type] userInfo:nil];
79 84
 	}
@@ -83,15 +88,16 @@
83 88
 	return result;
84 89
 }
85 90
 
86
-- (UIViewController<RNNRootViewProtocol> *)createComponent:(RNNLayoutNode*)node {
91
+- (UIViewController<RNNRootViewProtocol> *)createComponent:(RNNLayoutNode*)node nativeComponent:(BOOL)nativeComponent {
87 92
 	NSString* name = node.data[@"name"];
88 93
 	RNNNavigationOptions* options = [[RNNNavigationOptions alloc] initWithDict:node.data[@"options"]];
89 94
 	options.defaultOptions = _defaultOptions;
90 95
 	NSString* componentId = node.nodeId;
91
-	RNNRootViewController* component = [[RNNRootViewController alloc] initWithName:name withOptions:options withComponentId:componentId rootViewCreator:_creator eventEmitter:_eventEmitter];
92
-	CGSize availableSize = UIApplication.sharedApplication.delegate.window.bounds.size;
93
-	[_bridge.uiManager setAvailableSize:availableSize forRootView:component.view];
94
-	
96
+	RNNRootViewController* component = [[RNNRootViewController alloc] initWithName:name withOptions:options withComponentId:componentId rootViewCreator:_creator eventEmitter:_eventEmitter isNativeComponent:nativeComponent];
97
+	if (!component.isCustomViewController) {
98
+		CGSize availableSize = UIApplication.sharedApplication.delegate.window.bounds.size;
99
+		[_bridge.uiManager setAvailableSize:availableSize forRootView:component.view];
100
+	}
95 101
 	return component;
96 102
 }
97 103
 

+ 5
- 0
lib/ios/RNNCustomViewController.h 查看文件

@@ -0,0 +1,5 @@
1
+#import <UIKit/UIKit.h>
2
+
3
+@interface RNNCustomViewController : UIViewController
4
+
5
+@end

+ 18
- 0
lib/ios/RNNCustomViewController.m 查看文件

@@ -0,0 +1,18 @@
1
+#import "RNNCustomViewController.h"
2
+
3
+@implementation RNNCustomViewController
4
+
5
+- (void)viewDidLoad {
6
+    [super viewDidLoad];
7
+	[self addTestLabel];
8
+}
9
+
10
+- (void)addTestLabel {
11
+	UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
12
+	label.center = self.view.center;
13
+	label.text = @"Test label";
14
+	label.accessibilityIdentifier = @"TestLabel";
15
+	[self.view addSubview:label];
16
+}
17
+
18
+@end

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

@@ -12,6 +12,7 @@
12 12
 +(instancetype)create:(NSDictionary *)json;
13 13
 
14 14
 -(BOOL)isComponent;
15
+-(BOOL)isNativeComponent;
15 16
 -(BOOL)isStack;
16 17
 -(BOOL)isTabs;
17 18
 -(BOOL)isTopTabs;

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

@@ -17,6 +17,10 @@
17 17
 {
18 18
 	return [self.type isEqualToString:@"Component"];
19 19
 }
20
+-(BOOL)isNativeComponent
21
+{
22
+	return [self.type isEqualToString:@"NativeComponent"];
23
+}
20 24
 -(BOOL)isStack
21 25
 {
22 26
 	return [self.type isEqualToString:@"Stack"];

+ 5
- 1
lib/ios/RNNNavigationStackManager.m 查看文件

@@ -18,7 +18,11 @@ dispatch_queue_t RCTGetUIManagerQueue(void);
18 18
 -(void)push:(UIViewController<RNNRootViewProtocol> *)newTop onTop:(NSString *)componentId completion:(RNNTransitionCompletionBlock)completion {
19 19
 	UIViewController *vc = [_store findComponentForId:componentId];
20 20
 	[self preparePush:newTop onTopVC:vc completion:completion];
21
-	[self waitForContentToAppearAndThen:@selector(pushAfterLoad:)];
21
+	if ([newTop isCustomViewController]) {
22
+		[self pushAfterLoad:nil];
23
+	} else {
24
+		[self waitForContentToAppearAndThen:@selector(pushAfterLoad:)];
25
+	}
22 26
 }
23 27
 
24 28
 -(void)preparePush:(UIViewController<RNNRootViewProtocol> *)newTop onTopVC:(UIViewController*)vc completion:(RNNTransitionCompletionBlock)completion {

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

@@ -22,7 +22,8 @@
22 22
 				withOptions:(RNNNavigationOptions*)options
23 23
 			withComponentId:(NSString*)componentId
24 24
 			rootViewCreator:(id<RNNRootViewCreator>)creator
25
-			   eventEmitter:(RNNEventEmitter*)eventEmitter;
25
+			   eventEmitter:(RNNEventEmitter*)eventEmitter
26
+		  isNativeComponent:(BOOL)isNativeComponent;
26 27
 
27 28
 
28 29
 -(void)applyTabBarItem;

+ 36
- 2
lib/ios/RNNRootViewController.m 查看文件

@@ -7,6 +7,7 @@
7 7
 @interface RNNRootViewController()
8 8
 @property (nonatomic, strong) NSString* componentName;
9 9
 @property (nonatomic) BOOL _statusBarHidden;
10
+@property (nonatomic) BOOL isNativeComponent;
10 11
 @end
11 12
 
12 13
 @implementation RNNRootViewController
@@ -15,7 +16,8 @@
15 16
 				withOptions:(RNNNavigationOptions*)options
16 17
 			withComponentId:(NSString*)componentId
17 18
 			rootViewCreator:(id<RNNRootViewCreator>)creator
18
-			   eventEmitter:(RNNEventEmitter*)eventEmitter {
19
+			   eventEmitter:(RNNEventEmitter*)eventEmitter
20
+		  isNativeComponent:(BOOL)isNativeComponent {
19 21
 	self = [super init];
20 22
 	self.componentId = componentId;
21 23
 	self.componentName = name;
@@ -23,7 +25,13 @@
23 25
 	self.eventEmitter = eventEmitter;
24 26
 	self.animator = [[RNNAnimator alloc] initWithTransitionOptions:self.options.customTransition];
25 27
 	self.creator = creator;
26
-	self.view = [creator createRootView:self.componentName rootViewId:self.componentId];
28
+	self.isNativeComponent = isNativeComponent;
29
+	
30
+	if (self.isNativeComponent) {
31
+		[self addExternalVC:name];
32
+	} else {
33
+		self.view = [creator createRootView:self.componentName rootViewId:self.componentId];
34
+	}
27 35
 	
28 36
 	[[NSNotificationCenter defaultCenter] addObserver:self
29 37
 											 selector:@selector(onJsReload)
@@ -90,6 +98,10 @@
90 98
 	return self.options.animated ? [self.options.animated boolValue] : YES;
91 99
 }
92 100
 
101
+- (BOOL)isCustomViewController {
102
+	return self.isNativeComponent;
103
+}
104
+
93 105
 - (BOOL)prefersStatusBarHidden {
94 106
 	if ([self.options.statusBarHidden boolValue]) {
95 107
 		return YES;
@@ -143,6 +155,28 @@
143 155
 	[self.options.topTab applyOn:self];
144 156
 }
145 157
 
158
+-(void)addExternalVC:(NSString*)className {
159
+	if (className != nil) {
160
+		Class class = NSClassFromString(className);
161
+		if (class != NULL) {
162
+			id obj = [[class alloc] init];
163
+			if (obj != nil && [obj isKindOfClass:[UIViewController class]]) {
164
+				UIViewController *viewController = (UIViewController*)obj;
165
+				[self addChildViewController:viewController];
166
+				self.view = [[UIView alloc] init];
167
+				self.view.backgroundColor = [UIColor whiteColor];
168
+				[self.view addSubview:viewController.view];
169
+			}
170
+			else {
171
+				NSLog(@"addExternalVC: could not create instance. Make sure that your class is a UIViewController whihc confirms to RCCExternalViewControllerProtocol");
172
+			}
173
+		}
174
+		else {
175
+			NSLog(@"addExternalVC: could not create class from string. Check that the proper class name wass passed in ExternalNativeScreenClass");
176
+		}
177
+	}
178
+}
179
+
146 180
 /**
147 181
  *	fix for #877, #878
148 182
  */

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

@@ -4,9 +4,9 @@
4 4
 
5 5
 @optional
6 6
 - (void)mergeOptions:(NSDictionary*)options;
7
+- (BOOL)isCustomViewController;
7 8
 
8 9
 @required
9
-
10 10
 - (BOOL)isCustomTransitioned;
11 11
 - (BOOL)isAnimated;
12 12
 

+ 2
- 1
lib/ios/ReactNativeNavigationTests/RNNCommandsHandlerTest.m 查看文件

@@ -68,7 +68,8 @@
68 68
 																withOptions:initialOptions
69 69
 															withComponentId:@"componentId"
70 70
 															rootViewCreator:[[RNNTestRootViewCreator alloc] init]
71
-															   eventEmitter:nil];
71
+															   eventEmitter:nil
72
+														  isNativeComponent:NO];
72 73
 	RNNNavigationController* nav = [[RNNNavigationController alloc] initWithRootViewController:vc];
73 74
 	[vc viewWillAppear:false];
74 75
 	XCTAssertTrue([vc.navigationItem.title isEqual:@"the title"]);

+ 1
- 1
lib/ios/ReactNativeNavigationTests/RNNRootViewControllerTest.m 查看文件

@@ -42,7 +42,7 @@
42 42
 	self.componentId = @"cntId";
43 43
 	self.emitter = nil;
44 44
 	self.options = [RNNNavigationOptions new];
45
-	self.uut = [[RNNRootViewController alloc] initWithName:self.pageName withOptions:self.options withComponentId:self.componentId rootViewCreator:self.creator eventEmitter:self.emitter];
45
+	self.uut = [[RNNRootViewController alloc] initWithName:self.pageName withOptions:self.options withComponentId:self.componentId rootViewCreator:self.creator eventEmitter:self.emitter isNativeComponent:NO];
46 46
 }
47 47
 
48 48
 -(void)testTopBarBackgroundColor_validColor{

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

@@ -22,6 +22,14 @@ describe('LayoutTreeParser', () => {
22 22
       });
23 23
     });
24 24
 
25
+    it('native component', () => {
26
+      expect(uut.parse(LayoutExamples.nativeComponent)).toEqual({
27
+        type: LayoutType.NativeComponent,
28
+        data: { name: 'MyReactComponent', options: LayoutExamples.options, passProps: LayoutExamples.passProps },
29
+        children: []
30
+      });
31
+    });
32
+
25 33
     it('pass props', () => {
26 34
       const result = uut.parse({
27 35
         component: {
@@ -167,6 +175,14 @@ const singleComponent = {
167 175
   }
168 176
 };
169 177
 
178
+const nativeComponent = {
179
+  nativeComponent: {
180
+    name: 'MyReactComponent',
181
+    options,
182
+    passProps
183
+  }
184
+};
185
+
170 186
 const stackWithTopBar = {
171 187
   stack: {
172 188
     children: [
@@ -269,5 +285,6 @@ const LayoutExamples = {
269 285
   bottomTabs,
270 286
   sideMenu,
271 287
   topTabs,
272
-  complexLayout
288
+  complexLayout,
289
+  nativeComponent
273 290
 };

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

@@ -18,6 +18,8 @@ export class LayoutTreeParser {
18 18
       return this._stack(api.stack);
19 19
     } else if (api.component) {
20 20
       return this._component(api.component);
21
+    } else if (api.nativeComponent) {
22
+      return this._nativeComponent(api.nativeComponent);
21 23
     }
22 24
     throw new Error(`unknown LayoutType "${_.keys(api)}"`);
23 25
   }
@@ -96,4 +98,13 @@ export class LayoutTreeParser {
96 98
       children: []
97 99
     };
98 100
   }
101
+
102
+  _nativeComponent(api): LayoutNode {
103
+    return {
104
+      id: api.id,
105
+      type: LayoutType.NativeComponent,
106
+      data: { name: api.name, options: api.options, passProps: api.passProps },
107
+      children: []
108
+    };
109
+  }
99 110
 }

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

@@ -6,7 +6,8 @@ export enum LayoutType {
6 6
   SideMenuCenter = 'SideMenuCenter',
7 7
   SideMenuLeft = 'SideMenuLeft',
8 8
   SideMenuRight = 'SideMenuRight',
9
-  TopTabs = 'TopTabs'
9
+  TopTabs = 'TopTabs',
10
+  NativeComponent = 'NativeComponent'
10 11
 }
11 12
 
12 13
 export function isLayoutType(name: string): boolean {

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

@@ -11,6 +11,7 @@
11 11
 		13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
12 12
 		13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13 13
 		13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
14
+		50451D35204451A900695F00 /* RNNCustomViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 50451D34204451A800695F00 /* RNNCustomViewController.m */; };
14 15
 		7B8F30491E84151300110AEC /* libcxxreact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B8F2FDE1E840F3600110AEC /* libcxxreact.a */; };
15 16
 		7B8F304A1E84151300110AEC /* libjschelpers.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B8F2FE21E840F3600110AEC /* libjschelpers.a */; };
16 17
 		7B8F304B1E84151300110AEC /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B8F2FFA1E8411A800110AEC /* libRCTActionSheet.a */; };
@@ -270,6 +271,8 @@
270 271
 		13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
271 272
 		13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
272 273
 		13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
274
+		50451D33204451A800695F00 /* RNNCustomViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNNCustomViewController.h; path = ../../../lib/ios/RNNCustomViewController.h; sourceTree = "<group>"; };
275
+		50451D34204451A800695F00 /* RNNCustomViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNNCustomViewController.m; path = ../../../lib/ios/RNNCustomViewController.m; sourceTree = "<group>"; };
273 276
 		7B8F2FC41E840F1A00110AEC /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = ../../lib/ios/ReactNativeNavigation.xcodeproj; sourceTree = "<group>"; };
274 277
 		7B8F2FCA1E840F3600110AEC /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../../node_modules/react-native/React/React.xcodeproj"; sourceTree = "<group>"; };
275 278
 		7B8F2FE51E840F6700110AEC /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
@@ -315,6 +318,8 @@
315 318
 			children = (
316 319
 				13B07FAF1A68108700A75B9A /* AppDelegate.h */,
317 320
 				13B07FB01A68108700A75B9A /* AppDelegate.m */,
321
+				50451D33204451A800695F00 /* RNNCustomViewController.h */,
322
+				50451D34204451A800695F00 /* RNNCustomViewController.m */,
318 323
 				13B07FB51A68108700A75B9A /* Images.xcassets */,
319 324
 				13B07FB61A68108700A75B9A /* Info.plist */,
320 325
 				13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
@@ -850,6 +855,7 @@
850 855
 			isa = PBXSourcesBuildPhase;
851 856
 			buildActionMask = 2147483647;
852 857
 			files = (
858
+				50451D35204451A900695F00 /* RNNCustomViewController.m in Sources */,
853 859
 				13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
854 860
 				13B07FC11A68108700A75B9A /* main.m in Sources */,
855 861
 			);

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

@@ -23,6 +23,7 @@ class WelcomeScreen extends Component {
23 23
     this.onClickShowModal = this.onClickShowModal.bind(this);
24 24
     this.onClickLifecycleScreen = this.onClickLifecycleScreen.bind(this);
25 25
     this.onClickPushOptionsScreen = this.onClickPushOptionsScreen.bind(this);
26
+    this.onClickPushNativeComponent = this.onClickPushNativeComponent.bind(this);
26 27
     this.onClickPushOrientationMenuScreen = this.onClickPushOrientationMenuScreen.bind(this);
27 28
     this.onClickBackHandler = this.onClickBackHandler.bind(this);
28 29
     this.onClickPushTopTabsScreen = this.onClickPushTopTabsScreen.bind(this);
@@ -40,6 +41,7 @@ class WelcomeScreen extends Component {
40 41
         <Button title='Static Lifecycle Events' testID={testIDs.PUSH_STATIC_LIFECYCLE_BUTTON} onPress={this.onClickShowStaticLifecycleOverlay} />
41 42
         <Button title='Push' testID={testIDs.PUSH_BUTTON} onPress={this.onClickPush} />
42 43
         <Button title='Push Options Screen' testID={testIDs.PUSH_OPTIONS_BUTTON} onPress={this.onClickPushOptionsScreen} />
44
+        <Button title='Push Native Component' testID={testIDs.PUSH_NATIVE_COMPONENT_BUTTON} onPress={this.onClickPushNativeComponent} />
43 45
         {Platform.OS === 'android' && <Button title='Push Top Tabs screen' testID={testIDs.PUSH_TOP_TABS_BUTTON} onPress={this.onClickPushTopTabsScreen} />}
44 46
         {Platform.OS === 'android' && <Button title='Back Handler' testID={testIDs.BACK_HANDLER_BUTTON} onPress={this.onClickBackHandler} />}
45 47
         <Button title='Show Modal' testID={testIDs.SHOW_MODAL_BUTTON} onPress={this.onClickShowModal} />
@@ -231,6 +233,21 @@ class WelcomeScreen extends Component {
231 233
     });
232 234
   }
233 235
 
236
+  async onClickPushNativeComponent() {
237
+    await Navigation.push(this.props.componentId, {
238
+      nativeComponent: {
239
+        name: 'RNNCustomViewController',
240
+        options: {
241
+          topBar: {
242
+            title: 'pushed',
243
+            visible: true,
244
+            testID: testIDs.TOP_BAR_ELEMENT
245
+          }
246
+        }
247
+      }
248
+    });
249
+  }
250
+
234 251
   onClickLifecycleScreen() {
235 252
     Navigation.push(this.props.componentId, {
236 253
       component: {

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

@@ -53,6 +53,7 @@ module.exports = {
53 53
   OK_BUTTON: `OK_BUTTON`,
54 54
   MODAL_WITH_STACK_BUTTON: `MODAL_WITH_STACK_BUTTON`,
55 55
   CUSTOM_TRANSITION_BUTTON: `CUSTOM_TRANSITION_BUTTON`,
56
+  PUSH_NATIVE_COMPONENT_BUTTON: `PUSH_NATIVE_COMPONENT_BUTTON`,
56 57
 
57 58
   // Elements
58 59
   SCROLLVIEW_ELEMENT: `SCROLLVIEW_ELEMENT`,