From 46bea4a3cd0df532d5b93ded4a364368f6758c55 Mon Sep 17 00:00:00 2001 From: James Ide Date: Fri, 13 Feb 2015 12:01:44 -0800 Subject: [PATCH] [Initialization] Decouple script loading and environment setup from the root view Introduces RCTRootViewController, which handles the controller-appropriate work that RCTRootView was doing. This decouples the view from the script loading and JS environment setup (w/the bridge), which helps ReactKit be less of a framework and more of a library. The example apps have been updated and work. On the Facebook side you will need to replace RCTRootView with RCTRootViewController which should be fairly mechanical with VC containment. --- Examples/Movies/AppDelegate.m | 12 +- Examples/TicTacToe/AppDelegate.m | 12 +- Examples/UIExplorer/AppDelegate.m | 12 +- IntegrationTests/AppDelegate.m | 12 +- Libraries/RCTTest/RCTTestRunner.m | 15 +- ReactKit/Base/RCTRedBox.m | 2 +- ReactKit/Base/RCTRootView.h | 46 ---- ReactKit/Base/RCTRootView.m | 215 ----------------- ReactKit/Base/RCTRootViewController.h | 71 ++++++ ReactKit/Base/RCTRootViewController.m | 250 ++++++++++++++++++++ ReactKit/Base/RCTUtils.h | 3 + ReactKit/Base/RCTUtils.m | 5 +- ReactKit/Modules/RCTUIManager.h | 15 +- ReactKit/Modules/RCTUIManager.m | 36 +-- ReactKit/ReactKit.xcodeproj/project.pbxproj | 8 + 15 files changed, 392 insertions(+), 322 deletions(-) create mode 100644 ReactKit/Base/RCTRootViewController.h create mode 100644 ReactKit/Base/RCTRootViewController.m diff --git a/Examples/Movies/AppDelegate.m b/Examples/Movies/AppDelegate.m index c01fc2ca9f2bac..3442abdcdbf75e 100644 --- a/Examples/Movies/AppDelegate.m +++ b/Examples/Movies/AppDelegate.m @@ -2,14 +2,13 @@ #import "AppDelegate.h" -#import "RCTRootView.h" +#import "RCTRootViewController.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation; - RCTRootView *rootView = [[RCTRootView alloc] init]; // Loading JavaScript code - uncomment the one you want. @@ -30,13 +29,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // and uncomment the next following line // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; - rootView.scriptURL = jsCodeLocation; - rootView.moduleName = @"MoviesApp"; + RCTRootViewController *viewController = [[RCTRootViewController alloc] init]; + viewController.moduleName = @"MoviesApp"; + viewController.scriptURL = jsCodeLocation; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; - rootViewController.view = rootView; - self.window.rootViewController = rootViewController; + self.window.rootViewController = viewController; [self.window makeKeyAndVisible]; return YES; } diff --git a/Examples/TicTacToe/AppDelegate.m b/Examples/TicTacToe/AppDelegate.m index 52e6827524c66e..5e86b8de674342 100644 --- a/Examples/TicTacToe/AppDelegate.m +++ b/Examples/TicTacToe/AppDelegate.m @@ -2,14 +2,13 @@ #import "AppDelegate.h" -#import "RCTRootView.h" +#import "RCTRootViewController.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation; - RCTRootView *rootView = [[RCTRootView alloc] init]; // Loading JavaScript code - uncomment the one you want. @@ -30,13 +29,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // and uncomment the next following line // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; - rootView.scriptURL = jsCodeLocation; - rootView.moduleName = @"TicTacToeApp"; + RCTRootViewController *viewController = [[RCTRootViewController alloc] init]; + viewController.moduleName = @"TicTacToeApp"; + viewController.scriptURL = jsCodeLocation; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; - rootViewController.view = rootView; - self.window.rootViewController = rootViewController; + self.window.rootViewController = viewController; [self.window makeKeyAndVisible]; return YES; } diff --git a/Examples/UIExplorer/AppDelegate.m b/Examples/UIExplorer/AppDelegate.m index b280f1357f2d2a..a483be52ce1eb3 100644 --- a/Examples/UIExplorer/AppDelegate.m +++ b/Examples/UIExplorer/AppDelegate.m @@ -2,14 +2,13 @@ #import "AppDelegate.h" -#import "RCTRootView.h" +#import "RCTRootViewController.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation; - RCTRootView *rootView = [[RCTRootView alloc] init]; // Loading JavaScript code - uncomment the one you want. @@ -30,13 +29,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // and uncomment the next following line // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; - rootView.scriptURL = jsCodeLocation; - rootView.moduleName = @"UIExplorerApp"; + RCTRootViewController *viewController = [[RCTRootViewController alloc] init]; + viewController.moduleName = @"UIExplorerApp"; + viewController.scriptURL = jsCodeLocation; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; - rootViewController.view = rootView; - self.window.rootViewController = rootViewController; + self.window.rootViewController = viewController; [self.window makeKeyAndVisible]; return YES; } diff --git a/IntegrationTests/AppDelegate.m b/IntegrationTests/AppDelegate.m index db988faf8615d4..94be2d3932298a 100644 --- a/IntegrationTests/AppDelegate.m +++ b/IntegrationTests/AppDelegate.m @@ -2,14 +2,13 @@ #import "AppDelegate.h" -#import "RCTRootView.h" +#import "RCTRootViewController.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation; - RCTRootView *rootView = [[RCTRootView alloc] init]; // Loading JavaScript code - uncomment the one you want. @@ -30,13 +29,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // and uncomment the next following line // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; - rootView.scriptURL = jsCodeLocation; - rootView.moduleName = @"IntegrationTestsApp"; + RCTRootViewController *viewController = [[RCTRootViewController alloc] init]; + viewController.moduleName = @"IntegrationTestsApp"; + viewController.scriptURL = jsCodeLocation; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; - rootViewController.view = rootView; - self.window.rootViewController = rootViewController; + self.window.rootViewController = viewController; [self.window makeKeyAndVisible]; return YES; } diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 87da430103869f..23cf70c8343ac2 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -3,7 +3,7 @@ #import "RCTTestRunner.h" #import "RCTRedBox.h" -#import "RCTRootView.h" +#import "RCTRootViewController.h" #import "RCTTestModule.h" #import "RCTUtils.h" @@ -34,15 +34,14 @@ - (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps - (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock { RCTTestModule *testModule = [[RCTTestModule alloc] init]; - RCTRootView *rootView = [[RCTRootView alloc] init]; - UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; - vc.view = rootView; - rootView.moduleProvider = ^(void){ + RCTRootViewController *rootViewController = [[RCTRootViewController alloc] init]; + [[UIApplication sharedApplication].delegate window].rootViewController = rootViewController; + rootViewController.moduleProvider = ^(void){ return @[testModule]; }; - rootView.moduleName = moduleName; - rootView.initialProperties = initialProps; - rootView.scriptURL = [NSURL URLWithString:_script]; + rootViewController.moduleName = moduleName; + rootViewController.initialProperties = initialProps; + rootViewController.scriptURL = [NSURL URLWithString:_script]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage]; diff --git a/ReactKit/Base/RCTRedBox.m b/ReactKit/Base/RCTRedBox.m index 1c70394fdc65d2..2c2aa4b8b1a7e9 100644 --- a/ReactKit/Base/RCTRedBox.m +++ b/ReactKit/Base/RCTRedBox.m @@ -110,7 +110,7 @@ - (void)dismiss - (void)reload { - [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTReloadNotification" object:nil userInfo:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:self]; [self dismiss]; } diff --git a/ReactKit/Base/RCTRootView.h b/ReactKit/Base/RCTRootView.h index 3fc0be165074bf..acaf17a90569d7 100644 --- a/ReactKit/Base/RCTRootView.h +++ b/ReactKit/Base/RCTRootView.h @@ -2,52 +2,6 @@ #import -#import "RCTBridge.h" - @interface RCTRootView : UIView -/** - * The URL of the bundled application script (required). - * Setting this will clear the view contents, and trigger - * an asynchronous load/download and execution of the script. - */ -@property (nonatomic, strong) NSURL *scriptURL; - -/** - * The name of the JavaScript module to execute within the - * specified scriptURL (required). Setting this will not have - * any immediate effect, but it must be done prior to loading - * the script. - */ -@property (nonatomic, copy) NSString *moduleName; - -/** - * A block that returns an array of pre-allocated modules. These - * modules will take precedence over any automatically registered - * modules of the same name. - */ -@property (nonatomic, copy) RCTBridgeModuleProviderBlock moduleProvider; - -/** - * The default properties to apply to the view when the script bundle - * is first loaded. Defaults to nil/empty. - */ -@property (nonatomic, copy) NSDictionary *initialProperties; - -/** - * The class of the RCTJavaScriptExecutor to use with this view. - * If not specified, it will default to using RCTContextExecutor. - * Changes will take effect next time the bundle is reloaded. - */ -@property (nonatomic, strong) Class executorClass; - -/** - * Reload this root view, or all root views, respectively. - */ -- (void)reload; -+ (void)reloadAll; - -- (void)startOrResetInteractionTiming; -- (NSDictionary *)endAndResetInteractionTiming; - @end diff --git a/ReactKit/Base/RCTRootView.m b/ReactKit/Base/RCTRootView.m index c62793d503c240..637d3902006419 100644 --- a/ReactKit/Base/RCTRootView.m +++ b/ReactKit/Base/RCTRootView.m @@ -2,53 +2,9 @@ #import "RCTRootView.h" -#import "RCTBridge.h" -#import "RCTContextExecutor.h" -#import "RCTEventDispatcher.h" -#import "RCTKeyCommands.h" -#import "RCTLog.h" -#import "RCTRedBox.h" -#import "RCTSourceCode.h" -#import "RCTTouchHandler.h" -#import "RCTUIManager.h" -#import "RCTUtils.h" -#import "RCTWebViewExecutor.h" #import "UIView+ReactKit.h" -NSString *const RCTReloadNotification = @"RCTReloadNotification"; - @implementation RCTRootView -{ - RCTBridge *_bridge; - RCTTouchHandler *_touchHandler; - id _executor; -} - -static Class _globalExecutorClass; - -+ (void)initialize -{ - -#if DEBUG - - // Register Cmd-R as a global refresh key - [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"r" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - [self reloadAll]; - }]; - - // Cmd-D reloads using the web view executor, allows attaching from Safari dev tools. - [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"d" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - _globalExecutorClass = [RCTWebViewExecutor class]; - [self reloadAll]; - }]; - -#endif - -} - (id)initWithCoder:(NSCoder *)aDecoder { @@ -61,7 +17,6 @@ - (id)initWithCoder:(NSCoder *)aDecoder - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { - self.backgroundColor = [UIColor whiteColor]; [self setUp]; } return self; @@ -74,156 +29,6 @@ - (void)setUp static NSInteger rootViewTag = 1; self.reactTag = @(rootViewTag); rootViewTag += 10; - - // Add reload observer - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reload) - name:RCTReloadNotification - object:nil]; -} - -+ (NSArray *)JSMethods -{ - return @[ - @"AppRegistry.runApplication", - @"ReactIOS.unmountComponentAtNodeAndRemoveContainer" - ]; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" - args:@[self.reactTag]]; - - // TODO: eventually we'll want to be able to share the bridge between - // multiple rootviews, in which case we'll need to move this elsewhere - [_bridge invalidate]; -} - -- (void)bundleFinishedLoading:(NSError *)error -{ - if (error != nil) { - NSArray *stack = [[error userInfo] objectForKey:@"stack"]; - if (stack) { - [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] withStack:stack]; - } else { - [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] withDetails:[error localizedFailureReason]]; - } - } else { - - [_bridge.uiManager registerRootView:self]; - - NSString *moduleName = _moduleName ?: @""; - NSDictionary *appParameters = @{ - @"rootTag": self.reactTag, - @"initialProps": self.initialProperties ?: @{}, - }; - [_bridge enqueueJSCall:@"AppRegistry.runApplication" - args:@[moduleName, appParameters]]; - } -} - -- (void)loadBundle -{ - // Clear view - [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; - - if (!_scriptURL) { - return; - } - - // Clean up - [self removeGestureRecognizer:_touchHandler]; - [_touchHandler invalidate]; - [_executor invalidate]; - [_bridge invalidate]; - - // Choose local executor if specified, followed by global, followed by default - _executor = [[_executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class] alloc] init]; - _bridge = [[RCTBridge alloc] initWithExecutor:_executor moduleProvider:_moduleProvider]; - _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; - [self addGestureRecognizer:_touchHandler]; - - // Load the bundle - NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:_scriptURL completionHandler: - ^(NSData *data, NSURLResponse *response, NSError *error) { - - // Handle general request errors - if (error) { - if ([[error domain] isEqualToString:NSURLErrorDomain]) { - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: @"Could not connect to development server. Ensure node server is running - run 'npm start' from ReactKit root", - NSLocalizedFailureReasonErrorKey: [error localizedDescription], - NSUnderlyingErrorKey: error, - }; - error = [NSError errorWithDomain:@"JSServer" - code:error.code - userInfo:userInfo]; - } - [self bundleFinishedLoading:error]; - return; - } - - // Parse response as text - NSStringEncoding encoding = NSUTF8StringEncoding; - if (response.textEncodingName != nil) { - CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); - if (cfEncoding != kCFStringEncodingInvalidId) { - encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); - } - } - NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; - - // Handle HTTP errors - if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { - NSDictionary *userInfo; - NSDictionary *errorDetails = RCTJSONParse(rawText, nil); - if ([errorDetails isKindOfClass:[NSDictionary class]]) { - userInfo = @{ - NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", - @"stack": @[@{ - @"methodName": errorDetails[@"description"] ?: @"", - @"file": errorDetails[@"filename"] ?: @"", - @"lineNumber": errorDetails[@"lineNumber"] ?: @0 - }] - }; - } else { - userInfo = @{NSLocalizedDescriptionKey: rawText}; - } - error = [NSError errorWithDomain:@"JSServer" - code:[(NSHTTPURLResponse *)response statusCode] - userInfo:userInfo]; - - [self bundleFinishedLoading:error]; - return; - } - - // Success! - RCTSourceCode *sourceCodeModule = _bridge.modules[NSStringFromClass([RCTSourceCode class])]; - sourceCodeModule.scriptURL = _scriptURL; - sourceCodeModule.scriptText = rawText; - - [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^(NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self bundleFinishedLoading:error]; - }); - }]; - - }]; - - [task resume]; -} - -- (void)setScriptURL:(NSURL *)scriptURL -{ - if ([_scriptURL isEqual:scriptURL]) { - return; - } - - _scriptURL = scriptURL; - [self loadBundle]; } - (BOOL)isReactRootView @@ -231,24 +36,4 @@ - (BOOL)isReactRootView return YES; } -- (void)reload -{ - [self loadBundle]; -} - -+ (void)reloadAll -{ - [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; -} - -- (void)startOrResetInteractionTiming -{ - [_touchHandler startOrResetInteractionTiming]; -} - -- (NSDictionary *)endAndResetInteractionTiming -{ - return [_touchHandler endAndResetInteractionTiming]; -} - @end diff --git a/ReactKit/Base/RCTRootViewController.h b/ReactKit/Base/RCTRootViewController.h new file mode 100644 index 00000000000000..b93ac8e44b1a02 --- /dev/null +++ b/ReactKit/Base/RCTRootViewController.h @@ -0,0 +1,71 @@ +// Copyright 2004-present Facebook. All rights reserved. + +#import + +#import "RCTBridge.h" +#import "RCTJSMethodRegistrar.h" + +@class RCTRootView; + +/** + * Convenience class for initializing a React Native view controller. It creates + * default JavaScript executors and creates an `RCTRootView` for its view. + * + * The view controller also manages reloading the JavaScript source code and + * the contents of its `RCTRootView`. + * + * In more advanced applications, you may want to use `RCTRootView` and + * `RCTBridge` directly. + */ +@interface RCTRootViewController : UIViewController + +/** + * The same view as `self.view` but statically typed as `RCTRootView`. + */ +@property (nonatomic, strong, readonly) RCTRootView *reactRootView; + +/** + * The URL of the bundled application script (required). + * Setting this will clear the view contents, and trigger + * an asynchronous load/download and execution of the script. + */ +@property (nonatomic, strong) NSURL *scriptURL; + +/** + * The name of the JavaScript module to execute within the + * specified scriptURL (required). Setting this will not have + * any immediate effect, but it must be done prior to loading + * the script. + */ +@property (nonatomic, copy) NSString *moduleName; + +/** + * A block that returns an array of pre-allocated modules. These + * modules will take precedence over any automatically registered + * modules of the same name. + */ +@property (nonatomic, copy) RCTBridgeModuleProviderBlock moduleProvider; + +/** + * The default properties to apply to the view when the script bundle + * is first loaded. Defaults to nil/empty. + */ +@property (nonatomic, copy) NSDictionary *initialProperties; + +/** + * The class of the RCTJavaScriptExecutor to use with this view controller. + * If not specified, it will default to using RCTContextExecutor. + * Changes will take effect next time the bundle is reloaded. + */ +@property (nonatomic, strong) Class executorClass; + +/** + * Reload the root view, or all root views, respectively. + */ +- (void)reload; ++ (void)reloadAll; + +- (void)startOrResetInteractionTiming; +- (NSDictionary *)endAndResetInteractionTiming; + +@end diff --git a/ReactKit/Base/RCTRootViewController.m b/ReactKit/Base/RCTRootViewController.m new file mode 100644 index 00000000000000..068e2a5485af7f --- /dev/null +++ b/ReactKit/Base/RCTRootViewController.m @@ -0,0 +1,250 @@ +// Copyright 2004-present Facebook. All rights reserved. + +#import "RCTRootViewController.h" + +#import "RCTBridge.h" +#import "RCTContextExecutor.h" +#import "RCTEventDispatcher.h" +#import "RCTKeyCommands.h" +#import "RCTLog.h" +#import "RCTRedBox.h" +#import "RCTRootView.h" +#import "RCTSourceCode.h" +#import "RCTTouchHandler.h" +#import "RCTUIManager.h" +#import "RCTUtils.h" +#import "RCTWebViewExecutor.h" +#import "UIView+ReactKit.h" + +@implementation RCTRootViewController +{ + RCTBridge *_bridge; + RCTTouchHandler *_touchHandler; + id _executor; +} + +static Class _globalExecutorClass; + ++ (void)initialize +{ + +#if DEBUG + + // Register Cmd-R as a global refresh key + [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"r" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + [self reloadAll]; + }]; + + // Cmd-D reloads using the web view executor, allows attaching from Safari dev tools in iOS 7. + [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"d" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + _globalExecutorClass = [RCTWebViewExecutor class]; + [self reloadAll]; + }]; + +#endif + +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { + // Add reload observer + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reload) + name:RCTReloadNotification + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" + args:@[self.reactRootView.reactTag]]; + + // TODO: eventually we'll want to be able to share the bridge between + // multiple rootviews, in which case we'll need to move this elsewhere + [_bridge invalidate]; +} + +- (RCTRootView *)reactRootView +{ + NSAssert([self.view isKindOfClass:[RCTRootView class]], + @"RCTRootViewController's view must be an RCTRootView"); + return (RCTRootView *)self.view; +} + ++ (NSArray *)JSMethods +{ + return @[ + @"AppRegistry.runApplication", + @"ReactIOS.unmountComponentAtNodeAndRemoveContainer", + ]; +} + +#pragma mark - Life Cycle + +- (void)loadView +{ + self.view = [[RCTRootView alloc] init]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor whiteColor]; +} + +#pragma mark - Loading JavaScript and Rendering Components + +- (void)bundleFinishedLoading:(NSError *)error +{ + if (error != nil) { + NSArray *stack = [[error userInfo] objectForKey:@"stack"]; + if (stack) { + [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] withStack:stack]; + } else { + [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] withDetails:[error localizedFailureReason]]; + } + } else { + [_bridge.uiManager registerRootViewController:self]; + + NSString *moduleName = _moduleName ?: @""; + NSDictionary *appParameters = @{ + @"rootTag": self.reactRootView.reactTag, + @"initialProps": self.initialProperties ?: @{}, + }; + [_bridge enqueueJSCall:@"AppRegistry.runApplication" + args:@[moduleName, appParameters]]; + } +} + +- (void)loadBundle +{ + // Clear view + [self.view.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + + if (!_scriptURL) { + return; + } + + // Clean up + [self.view removeGestureRecognizer:_touchHandler]; + [_touchHandler invalidate]; + [_executor invalidate]; + [_bridge invalidate]; + + // Choose local executor if specified, followed by global, followed by default + _executor = [[_executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class] alloc] init]; + _bridge = [[RCTBridge alloc] initWithExecutor:_executor moduleProvider:_moduleProvider]; + _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; + [self.view addGestureRecognizer:_touchHandler]; + + // Load the bundle + NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:_scriptURL completionHandler: + ^(NSData *data, NSURLResponse *response, NSError *error) { + + // Handle general request errors + if (error) { + if ([[error domain] isEqualToString:NSURLErrorDomain]) { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: @"Could not connect to development server. Ensure node server is running - run 'npm start' from ReactKit root", + NSLocalizedFailureReasonErrorKey: [error localizedDescription], + NSUnderlyingErrorKey: error, + }; + error = [NSError errorWithDomain:@"JSServer" + code:error.code + userInfo:userInfo]; + } + [self bundleFinishedLoading:error]; + return; + } + + // Parse response as text + NSStringEncoding encoding = NSUTF8StringEncoding; + if (response.textEncodingName != nil) { + CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + if (cfEncoding != kCFStringEncodingInvalidId) { + encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); + } + } + NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; + + // Handle HTTP errors + if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { + NSDictionary *userInfo; + NSDictionary *errorDetails = RCTJSONParse(rawText, nil); + if ([errorDetails isKindOfClass:[NSDictionary class]]) { + userInfo = @{ + NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", + @"stack": @[@{ + @"methodName": errorDetails[@"description"] ?: @"", + @"file": errorDetails[@"filename"] ?: @"", + @"lineNumber": errorDetails[@"lineNumber"] ?: @0 + }] + }; + } else { + userInfo = @{NSLocalizedDescriptionKey: rawText}; + } + error = [NSError errorWithDomain:@"JSServer" + code:[(NSHTTPURLResponse *)response statusCode] + userInfo:userInfo]; + + [self bundleFinishedLoading:error]; + return; + } + + // Success! + RCTSourceCode *sourceCodeModule = _bridge.modules[NSStringFromClass([RCTSourceCode class])]; + sourceCodeModule.scriptURL = _scriptURL; + sourceCodeModule.scriptText = rawText; + + [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^(NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self bundleFinishedLoading:error]; + }); + }]; + + }]; + + [task resume]; +} + +- (void)setScriptURL:(NSURL *)scriptURL +{ + if ([_scriptURL isEqual:scriptURL]) { + return; + } + + _scriptURL = scriptURL; + [self loadBundle]; +} + +- (void)reload +{ + [self loadBundle]; +} + ++ (void)reloadAll +{ + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; +} + +- (void)startOrResetInteractionTiming +{ + [_touchHandler startOrResetInteractionTiming]; +} + +- (NSDictionary *)endAndResetInteractionTiming +{ + return [_touchHandler endAndResetInteractionTiming]; +} + +@end diff --git a/ReactKit/Base/RCTUtils.h b/ReactKit/Base/RCTUtils.h index adf35cb9b80d91..f722bed953e461 100644 --- a/ReactKit/Base/RCTUtils.h +++ b/ReactKit/Base/RCTUtils.h @@ -11,6 +11,9 @@ extern "C" { #endif +// NSNotification specifying that the application should reload its JavaScript and views +extern NSString *const RCTReloadNotification; + // Utility functions for JSON object <-> string serialization/deserialization NSString *RCTJSONStringify(id jsonObject, NSError **error); id RCTJSONParse(NSString *jsonString, NSError **error); diff --git a/ReactKit/Base/RCTUtils.m b/ReactKit/Base/RCTUtils.m index 217368e171cb19..4e26d5388ffb20 100644 --- a/ReactKit/Base/RCTUtils.m +++ b/ReactKit/Base/RCTUtils.m @@ -5,12 +5,13 @@ #import #import -#import - #import +#import #import "RCTLog.h" +NSString *const RCTReloadNotification = @"RCTReload"; + NSString *RCTJSONStringify(id jsonObject, NSError **error) { NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:error]; diff --git a/ReactKit/Modules/RCTUIManager.h b/ReactKit/Modules/RCTUIManager.h index 701c37f930aacc..9378b59c9837de 100644 --- a/ReactKit/Modules/RCTUIManager.h +++ b/ReactKit/Modules/RCTUIManager.h @@ -7,7 +7,7 @@ #import "RCTInvalidating.h" #import "RCTViewManager.h" -@class RCTRootView; +@class RCTRootViewController; @protocol RCTScrollableProtocol; @@ -25,11 +25,16 @@ @property (nonatomic, readwrite, weak) id nativeMainScrollDelegate; /** - * Register a root view with the RCTUIManager. Theoretically, a single manager - * can support multiple root views, however this feature is not currently exposed - * and may eventually be removed. + * Register a root view controller with the RCTUIManager. Theoretically, a + * single manager can support multiple root views, however this feature is not + * currently exposed and may eventually be removed. */ -- (void)registerRootView:(RCTRootView *)rootView; +- (void)registerRootViewController:(RCTRootViewController *)rootViewController; + +/** + * Removes the given root view controller from the RCTUIManager. + */ +- (void)removeRootViewController:(RCTRootViewController *)rootViewController; /** * Schedule a block to be executed on the UI thread. Useful if you need to execute diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index 93090e2c17b0d5..e56edcc4c1873a 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -14,6 +14,7 @@ #import "RCTLog.h" #import "RCTNavigator.h" #import "RCTRootView.h" +#import "RCTRootViewController.h" #import "RCTScrollableProtocol.h" #import "RCTShadowView.h" #import "RCTSparseArray.h" @@ -155,7 +156,7 @@ @implementation RCTUIManager dispatch_queue_t _shadowQueue; // Root views are only mutated on the shadow queue - NSMutableSet *_rootViewTags; + NSMutableSet *_rootViewControllers; NSMutableArray *_pendingUIBlocks; NSLock *_pendingUIBlocksLock; @@ -218,7 +219,7 @@ - (instancetype)init // Internal resources _pendingUIBlocks = [[NSMutableArray alloc] init]; - _rootViewTags = [[NSMutableSet alloc] init]; + _rootViewControllers = [[NSMutableSet alloc] init]; } return self; } @@ -270,10 +271,11 @@ - (void)invalidate [_pendingUIBlocksLock unlock]; } -- (void)registerRootView:(RCTRootView *)rootView; +- (void)registerRootViewController:(RCTRootViewController *)rootViewController; { RCTAssertMainThread(); + UIView *rootView = rootViewController.view; NSNumber *reactTag = rootView.reactTag; UIView *existingView = _viewRegistry[reactTag]; RCTCAssert(existingView == nil || existingView == rootView, @@ -296,7 +298,7 @@ - (void)registerRootView:(RCTRootView *)rootView; shadowView.reactRootView = YES; // can this just be inferred from the fact that it has no superview? _shadowViewRegistry[shadowView.reactTag] = shadowView; - [_rootViewTags addObject:reactTag]; + [_rootViewControllers addObject:rootViewController]; }); } @@ -536,14 +538,15 @@ - (void)_removeChildren:(NSArray *)children fromContainer:(id