From f370f9cbc458fac77019298ff63c29def5efc4a9 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Thu, 2 Apr 2015 07:33:21 -0700 Subject: [PATCH] [ReactNative] Unfork RKRootView --- Libraries/RCTTest/RCTTestModule.h | 2 + Libraries/RCTTest/RCTTestRunner.m | 30 +- .../RCTWebSocketExecutor.m | 1 + React/Base/RCTBridge.h | 10 + React/Base/RCTBridge.m | 118 +++++- React/Base/RCTDevMenu.h | 4 +- React/Base/RCTDevMenu.m | 22 +- React/Base/RCTJavaScriptLoader.h | 22 ++ React/Base/RCTJavaScriptLoader.m | 135 +++++++ React/Base/RCTRootView.h | 34 +- React/Base/RCTRootView.m | 352 ++++++------------ React/Executors/RCTContextExecutor.h | 2 +- React/Modules/RCTUIManager.h | 3 +- React/Modules/RCTUIManager.m | 5 +- React/React.xcodeproj/project.pbxproj | 6 + 15 files changed, 448 insertions(+), 298 deletions(-) create mode 100755 React/Base/RCTJavaScriptLoader.h create mode 100755 React/Base/RCTJavaScriptLoader.m diff --git a/Libraries/RCTTest/RCTTestModule.h b/Libraries/RCTTest/RCTTestModule.h index 0f5adcd2c..21ed60c6b 100644 --- a/Libraries/RCTTest/RCTTestModule.h +++ b/Libraries/RCTTest/RCTTestModule.h @@ -21,6 +21,8 @@ // This is used to give meaningful names to snapshot image files. @property (nonatomic, assign) SEL testSelector; +@property (nonatomic, weak) UIView *view; + - (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view; @end diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 27aac48d9..8cb5169c3 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -17,15 +17,6 @@ #define TIMEOUT_SECONDS 240 -@interface RCTRootView (Testing) - -- (instancetype)_initWithBundleURL:(NSURL *)bundleURL - moduleName:(NSString *)moduleName - launchOptions:(NSDictionary *)launchOptions - moduleProvider:(RCTBridgeModuleProviderBlock)moduleProvider; - -@end - @implementation RCTTestRunner { FBSnapshotTestController *_snapshotController; @@ -75,17 +66,17 @@ RCTTestModule *testModule = [[RCTTestModule alloc] initWithSnapshotController:_snapshotController view:nil]; testModule.testSelector = test; - - RCTRootView *rootView = [[RCTRootView alloc] _initWithBundleURL:[NSURL URLWithString:_script] - moduleName:moduleName - launchOptions:nil - moduleProvider:^{ - return @[testModule]; - }]; - [testModule setValue:rootView forKey:@"_view"]; - rootView.frame = CGRectMake(0, 0, 320, 2000); + RCTBridge *bridge = [[RCTBridge alloc] initWithBundlePath:_script + moduleProvider:^(){ + return @[testModule]; + } + launchOptions:nil]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge + moduleName:moduleName]; + testModule.view = rootView; [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized rootView.initialProperties = initialProps; + rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage]; @@ -94,8 +85,9 @@ [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; error = [[RCTRedBox sharedInstance] currentErrorMessage]; } - [rootView invalidate]; [rootView removeFromSuperview]; + [rootView.bridge invalidate]; + [rootView invalidate]; RCTAssert(vc.view.subviews.count == 0, @"There shouldn't be any other views: %@", vc.view); vc.view = nil; [[RCTRedBox sharedInstance] dismiss]; diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m index 933c49aed..ddf973b24 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -168,6 +168,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); - (void)invalidate { + [_jsQueue cancelAllOperations]; _socket.delegate = nil; [_socket closeWithCode:1000 reason:@"Invalidated"]; _socket = nil; diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 7aa28e49b..05767533d 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import "RCTBridgeModule.h" #import "RCTInvalidating.h" #import "RCTJavaScriptExecutor.h" @@ -24,11 +26,15 @@ */ typedef NSArray *(^RCTBridgeModuleProviderBlock)(void); +extern NSString *const RCTReloadBridge; + /** * Async batched bridge used to communicate with the JavaScript application. */ @interface RCTBridge : NSObject +@property (nonatomic, assign, readonly, getter=isLoaded) BOOL loaded; + /** * The designated initializer. This creates a new bridge on top of the specified * executor. The bridge should then be used for all subsequent communication @@ -55,6 +61,8 @@ typedef NSArray *(^RCTBridgeModuleProviderBlock)(void); */ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete; +@property (nonatomic, strong) Class executorClass; + /** * The event dispatcher is a wrapper around -enqueueJSCall:args: that provides a * higher-level interface for sending UI events such as touches and text input. @@ -89,4 +97,6 @@ typedef NSArray *(^RCTBridgeModuleProviderBlock)(void); */ + (BOOL)hasValidJSExecutor; +- (void)reload; + @end diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 148a5d933..422a3da44 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -16,11 +16,16 @@ #import #import +#import "RCTContextExecutor.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" +#import "RCTJavaScriptLoader.h" +#import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTRootView.h" #import "RCTSparseArray.h" #import "RCTUtils.h" +#import "RCTWebViewExecutor.h" /** * Must be kept in sync with `MessageQueue.js`. @@ -34,6 +39,8 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; +NSString *const RCTReloadBridge = @"RCTReloadBridge"; + /** * This function returns the module name for a given class. */ @@ -125,6 +132,8 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) NSString *_methodName; } +static Class _globalExecutorClass; + - (instancetype)initWithMethodName:(NSString *)methodName JSMethodName:(NSString *)JSMethodName { @@ -497,33 +506,41 @@ static NSDictionary *RCTLocalModulesConfig() RCTSparseArray *_modulesByID; NSDictionary *_modulesByName; id _javaScriptExecutor; + Class _executorClass; + NSString *_bundlePath; + NSDictionary *_launchOptions; RCTBridgeModuleProviderBlock _moduleProvider; + BOOL _loaded; } static id _latestJSExecutor; -- (instancetype)initWithBundlePath:(NSString *)bundlepath +- (instancetype)initWithBundlePath:(NSString *)bundlePath moduleProvider:(RCTBridgeModuleProviderBlock)block launchOptions:(NSDictionary *)launchOptions { if ((self = [super init])) { - _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; - _shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL); + _bundlePath = bundlePath; _moduleProvider = block; _launchOptions = launchOptions; + [self setUp]; + [self bindKeys]; } + return self; } - -- (void)setJavaScriptExecutor:(id)executor -{ - _javaScriptExecutor = executor; - _latestJSExecutor = _javaScriptExecutor; - [self setUp]; -} - - (void)setUp { + Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class]; + if ([NSStringFromClass(executorClass) isEqualToString:@"RCTWebViewExecutor"]) { + _javaScriptExecutor = [[RCTWebViewExecutor alloc] initWithWebView:[[UIWebView alloc] init]]; + } else { + _javaScriptExecutor = [[executorClass alloc] init]; + } + _latestJSExecutor = _javaScriptExecutor; + _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; + _shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL); + // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; for (id module in _moduleProvider ? _moduleProvider() : nil) { @@ -574,11 +591,58 @@ static id _latestJSExecutor; dispatch_semaphore_signal(semaphore); }]; - if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) { - RCTLogError(@"JavaScriptExecutor took too long to inject JSON object"); + + if (_bundlePath != nil) { // Allow testing without a script + RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; + [loader loadBundleAtURL:[NSURL URLWithString:_bundlePath] + onComplete:^(NSError *error) { + _loaded = YES; + 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 { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:self]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reload) + name:RCTReloadNotification + object:nil]; + ; + } + }]; } } +- (void)bindKeys +{ +#if TARGET_IPHONE_SIMULATOR + [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"r" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + [self reload]; + }]; + [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"n" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + _executorClass = Nil; + [self reload]; + }]; + + [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"d" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + _executorClass = NSClassFromString(@"RCTWebSocketExecutor"); + if (!_executorClass) { + RCTLogError(@"WebSocket debugger is not available. Did you forget to include RCTWebSocketExecutor?"); + } + [self reload]; + }]; +#endif +} - (NSDictionary *)modules { @@ -602,6 +666,13 @@ static id _latestJSExecutor; - (void)invalidate { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + // Wait for queued methods to finish + dispatch_sync(self.shadowQueue, ^{ + // Make sure all dispatchers have been executed before continuing + }); + // Release executor if (_latestJSExecutor == _javaScriptExecutor) { _latestJSExecutor = nil; @@ -609,11 +680,6 @@ static id _latestJSExecutor; [_javaScriptExecutor invalidate]; _javaScriptExecutor = nil; - // Wait for queued methods to finish - dispatch_sync(self.shadowQueue, ^{ - // Make sure all dispatchers have been executed before continuing - }); - // Invalidate modules for (id target in _modulesByID.allObjects) { if ([target respondsToSelector:@selector(invalidate)]) { @@ -624,6 +690,7 @@ static id _latestJSExecutor; // Release modules (breaks retain cycle if module has strong bridge reference) _modulesByID = nil; _modulesByName = nil; + _loaded = NO; } /** @@ -647,9 +714,11 @@ static id _latestJSExecutor; NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); + if (self.loaded) { [self _invokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" arguments:@[moduleID, methodID, args ?: @[]]]; + } } - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete @@ -793,6 +862,19 @@ static id _latestJSExecutor; return YES; } +- (void)reload +{ + if (_loaded) { + // If the bridge has not loaded yet, the context will be already invalid at + // the time the javascript gets executed. + // It will crash the javascript, and even the next `load` won't render. + [self invalidate]; + [self setUp]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadViewsNotification + object:self]; + } +} + + (BOOL)hasValidJSExecutor { return (_latestJSExecutor != nil && [_latestJSExecutor isValid]); diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index e7d3b8b30..a49e076e6 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -9,11 +9,11 @@ #import -@class RCTRootView; +@class RCTBridge; @interface RCTDevMenu : NSObject -- (instancetype)initWithRootView:(RCTRootView *)rootView; +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; - (void)show; @end diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 77ce73935..5fe58f608 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -11,28 +11,29 @@ #import "RCTRedBox.h" #import "RCTRootView.h" +#import "RCTSourceCode.h" @interface RCTDevMenu () { BOOL _liveReload; } -@property (nonatomic, weak) RCTRootView *view; +@property (nonatomic, weak) RCTBridge *bridge; @end @implementation RCTDevMenu -- (instancetype)initWithRootView:(RCTRootView *)rootView +- (instancetype)initWithBridge:(RCTBridge *)bridge { if (self = [super init]) { - self.view = rootView; + _bridge = bridge; } return self; } - (void)show { - NSString *debugTitle = self.view.executorClass == nil ? @"Enable Debugging" : @"Disable Debugging"; + NSString *debugTitle = self.bridge.executorClass == Nil ? @"Enable Debugging" : @"Disable Debugging"; NSString *liveReloadTitle = _liveReload ? @"Disable Live Reload" : @"Enable Live Reload"; UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development" delegate:self @@ -40,16 +41,16 @@ destructiveButtonTitle:nil otherButtonTitles:@"Reload", debugTitle, liveReloadTitle, nil]; actionSheet.actionSheetStyle = UIBarStyleBlack; - [actionSheet showInView:self.view]; + [actionSheet showInView:[[[[UIApplication sharedApplication] keyWindow] rootViewController] view]]; } - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == 0) { - [self.view reload]; + [self.bridge reload]; } else if (buttonIndex == 1) { - self.view.executorClass = self.view.executorClass == nil ? NSClassFromString(@"RCTWebSocketExecutor") : nil; - [self.view reload]; + self.bridge.executorClass = self.bridge.executorClass == Nil ? NSClassFromString(@"RCTWebSocketExecutor") : nil; + [self.bridge reload]; } else if (buttonIndex == 2) { _liveReload = !_liveReload; [self _pollAndReload]; @@ -59,7 +60,8 @@ - (void)_pollAndReload { if (_liveReload) { - NSURL *url = [self.view scriptURL]; + RCTSourceCode *sourceCodeModule = self.bridge.modules[NSStringFromClass([RCTSourceCode class])]; + NSURL *url = sourceCodeModule.scriptURL; NSURL *longPollURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:url]; [self performSelectorInBackground:@selector(_checkForUpdates:) withObject:longPollURL]; } @@ -75,7 +77,7 @@ dispatch_async(dispatch_get_main_queue(), ^{ if (_liveReload && response.statusCode == 205) { [[RCTRedBox sharedInstance] dismiss]; - [self.view reload]; + [self.bridge reload]; } [self _pollAndReload]; }); diff --git a/React/Base/RCTJavaScriptLoader.h b/React/Base/RCTJavaScriptLoader.h new file mode 100755 index 000000000..7c750c585 --- /dev/null +++ b/React/Base/RCTJavaScriptLoader.h @@ -0,0 +1,22 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "RCTJavaScriptExecutor.h" + +@class RCTBridge; + +/** + * Class that allows easy embedding, loading, life-cycle management of a + * JavaScript application inside of a native application. + * TODO: Before loading new application source, publish global notification in + * JavaScript so that applications can clean up resources. (launch blocker). + * TODO: Incremental module loading. (low pri). + */ +@interface RCTJavaScriptLoader : NSObject + +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; + +- (void)loadBundleAtURL:(NSURL *)moduleURL onComplete:(RCTJavaScriptCompleteBlock)onComplete; + +@end diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m new file mode 100755 index 000000000..c8fce940c --- /dev/null +++ b/React/Base/RCTJavaScriptLoader.m @@ -0,0 +1,135 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTJavaScriptLoader.h" + +#import "RCTBridge.h" +#import "RCTInvalidating.h" +#import "RCTLog.h" +#import "RCTRedBox.h" +#import "RCTSourceCode.h" +#import "RCTUtils.h" + +#define NO_REMOTE_MODULE @"Could not fetch module bundle %@. Ensure node server is running.\n\nIf it timed out, try reloading." +#define NO_LOCAL_BUNDLE @"Could not load local bundle %@. Ensure file exists." + +#define CACHE_DIR @"RCTJSBundleCache" + +#pragma mark - Application Engine + +/** + * TODO: + * - Add window resize rotation events matching the DOM API. + * - Device pixel ration hooks. + * - Source maps. + */ +@implementation RCTJavaScriptLoader +{ + RCTBridge *_bridge; +} + +/** + * `CADisplayLink` code copied from Ejecta but we've placed the JavaScriptCore + * engine in its own dedicated thread. + * + * TODO: Try adding to the `RCTJavaScriptExecutor`'s thread runloop. Removes one + * additional GCD dispatch per frame and likely makes it so that other UIThread + * operations don't delay the dispatch (so we can begin working in JS much + * faster.) Event handling must still be sent via a GCD dispatch, of course. + * + * We must add the display link to two runloops in order to get setTimeouts to + * fire during scrolling. (`NSDefaultRunLoopMode` and `UITrackingRunLoopMode`) + * TODO: We can invent a `requestAnimationFrame` and + * `requestAvailableAnimationFrame` to control if callbacks can be fired during + * an animation. + * http://stackoverflow.com/questions/12622800/why-does-uiscrollview-pause-my-cadisplaylink + * + */ +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + RCTAssertMainThread(); + if (self = [super init]) { + _bridge = bridge; + } + return self; +} + +- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete +{ + if (!scriptURL) { + NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" + code:1 + userInfo:@{NSLocalizedDescriptionKey: @"No script URL provided"}]; + onComplete(error); + return; + } + + 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]; + } + onComplete(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]; + + onComplete(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(), ^{ + onComplete(error); + }); + }]; + + }]; + + [task resume]; +} + +@end diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index e5776cc6f..b9e91b7a0 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -11,18 +11,25 @@ #import "RCTBridge.h" -@interface RCTRootView : UIView +extern NSString *const RCTJavaScriptDidLoadNotification; +extern NSString *const RCTReloadNotification; +extern NSString *const RCTReloadViewsNotification; -- (instancetype)initWithBundleURL:(NSURL *)bundleURL - moduleName:(NSString *)moduleName - launchOptions:(NSDictionary *)launchOptions /* NS_DESIGNATED_INITIALIZER */; +@interface RCTRootView : UIView + +- (instancetype)initWithBridge:(RCTBridge *)bridge + moduleName:(NSString *)moduleName NS_DESIGNATED_INITIALIZER; /** - * 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. + * - Convenience initializer - + * A bridge will be created internally. + * This initializer is intended to be used when the app has a single RCTRootView, + * otherwise create an `RCTBridge` and pass it in via `initWithBridge:moduleName:` + * to all the instances. */ -@property (nonatomic, strong, readonly) NSURL *scriptURL; +- (instancetype)initWithBundleURL:(NSURL *)bundleURL + moduleName:(NSString *)moduleName + launchOptions:(NSDictionary *)launchOptions; /** * The name of the JavaScript module to execute within the @@ -32,12 +39,7 @@ */ @property (nonatomic, copy, readonly) 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, readonly) RCTBridgeModuleProviderBlock moduleProvider; +@property (nonatomic, strong, readonly) RCTBridge *bridge; /** * The default properties to apply to the view when the script bundle @@ -64,6 +66,10 @@ - (void)reload; + (void)reloadAll; +@property (nonatomic, weak) UIViewController *backingViewController; + +@property (nonatomic, strong, readonly) UIView *contentView; + - (void)startOrResetInteractionTiming; - (NSDictionary *)endAndResetInteractionTiming; diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index ac39a9aed..9d913c048 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -9,6 +9,8 @@ #import "RCTRootView.h" +#import + #import "RCTBridge.h" #import "RCTContextExecutor.h" #import "RCTDevMenu.h" @@ -23,7 +25,9 @@ #import "RCTWebViewExecutor.h" #import "UIView+React.h" +NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; NSString *const RCTReloadNotification = @"RCTReloadNotification"; +NSString *const RCTReloadViewsNotification = @"RCTReloadViewsNotification"; /** * HACK(t6568049) This should be removed soon, hiding to prevent people from @@ -35,95 +39,113 @@ NSString *const RCTReloadNotification = @"RCTReloadNotification"; @end +@interface RCTUIManager (RCTRootView) + +- (NSNumber *)allocateRootTag; + +@end + @implementation RCTRootView { RCTDevMenu *_devMenu; RCTBridge *_bridge; RCTTouchHandler *_touchHandler; - id _executor; + NSString *_moduleName; BOOL _registered; NSDictionary *_launchOptions; + UIView *_contentView; } -static Class _globalExecutorClass; - -+ (void)initialize +- (instancetype)initWithBridge:(RCTBridge *)bridge + moduleName:(NSString *)moduleName { + RCTAssert(bridge, @"A bridge instance is required to create an RCTRootView"); + RCTAssert(moduleName, @"A moduleName is required to create an RCTRootView"); -#if TARGET_IPHONE_SIMULATOR - - // 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 = NSClassFromString(@"RCTWebSocketExecutor"); - if (!_globalExecutorClass) { - RCTLogError(@"WebSocket debugger is not available. Did you forget to include RCTWebSocketExecutor?"); - } - [self reloadAll]; - }]; - + if ((self = [super init])) { +#ifdef DEBUG + _enableDevMenu = YES; #endif - + _bridge = bridge; + _moduleName = moduleName; + self.backgroundColor = [UIColor whiteColor]; + [self setUp]; + } + return self; } - (instancetype)initWithBundleURL:(NSURL *)bundleURL moduleName:(NSString *)moduleName launchOptions:(NSDictionary *)launchOptions { - if ((self = [super init])) { - RCTAssert(bundleURL, @"A bundleURL is required to create an RCTRootView"); - RCTAssert(moduleName, @"A bundleURL is required to create an RCTRootView"); - _moduleName = moduleName; - _launchOptions = launchOptions; - [self setUp]; - [self setScriptURL:bundleURL]; - } - return self; + RCTBridge *bridge = [[RCTBridge alloc] initWithBundlePath:bundleURL.absoluteString + moduleProvider:nil + launchOptions:launchOptions]; + return [self initWithBridge:bridge + moduleName:moduleName]; } - /** - * HACK(t6568049) Private constructor for testing purposes - */ -- (instancetype)_initWithBundleURL:(NSURL *)bundleURL - moduleName:(NSString *)moduleName - launchOptions:(NSDictionary *)launchOptions - moduleProvider:(RCTBridgeModuleProviderBlock)moduleProvider +- (void)dealloc { - if ((self = [super init])) { - _moduleProvider = moduleProvider; - _moduleName = moduleName; - _launchOptions = launchOptions; - [self setUp]; - [self setScriptURL:bundleURL]; - } - return self; + [self tearDown]; } - (void)setUp { - // Every root view that is created must have a unique react tag. - // Numbering of these tags goes from 1, 11, 21, 31, etc - static NSInteger rootViewTag = 1; - self.reactTag = @(rootViewTag); -#ifdef DEBUG - self.enableDevMenu = YES; -#endif - self.backgroundColor = [UIColor whiteColor]; - rootViewTag += 10; + if (!_registered) { + /** + * Every root view that is created must have a unique react tag. + * Numbering of these tags goes from 1, 11, 21, 31, etc + * + * NOTE: Since the bridge persists, the RootViews might be reused, so now + * the react tag is assigned every time we load new content. + */ + _contentView = [[UIView alloc] init]; + _contentView.reactTag = [_bridge.uiManager allocateRootTag]; + _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; + [_contentView addGestureRecognizer:_touchHandler]; + [self addSubview:_contentView]; - // Add reload observer - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reload) - name:RCTReloadNotification - object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reload) + name:RCTReloadViewsNotification + object:_bridge]; + if (_bridge.loaded) { + [self bundleFinishedLoading]; + } else { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(bundleFinishedLoading) + name:RCTJavaScriptDidLoadNotification + object:_bridge]; + } + } +} + +- (void)tearDown +{ + if (_registered) { + _registered = NO; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_contentView removeGestureRecognizer:_touchHandler]; + [_contentView removeFromSuperview]; + [_touchHandler invalidate]; + [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" + args:@[_contentView.reactTag]]; + } +} + +- (BOOL)isValid +{ + return _registered; +} + +- (void)invalidate +{ + [self tearDown]; +} + +- (UIViewController *)backingViewController { + return _backingViewController ?: [super backingViewController]; } - (BOOL)canBecomeFirstResponder @@ -135,7 +157,7 @@ static Class _globalExecutorClass; { if (motion == UIEventSubtypeMotionShake && self.enableDevMenu) { if (!_devMenu) { - _devMenu = [[RCTDevMenu alloc] initWithRootView:self]; + _devMenu = [[RCTDevMenu alloc] initWithBridge:self.bridge]; } [_devMenu show]; } @@ -149,194 +171,51 @@ static Class _globalExecutorClass; ]; } -- (void)dealloc +- (void)bundleFinishedLoading { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" - args:@[self.reactTag]]; - [self invalidate]; -} - -#pragma mark - RCTInvalidating - -- (BOOL)isValid -{ - return [_bridge isValid]; -} - -- (void)invalidate -{ - // Clear view - [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; - - [self removeGestureRecognizer:_touchHandler]; - [_touchHandler invalidate]; - [_executor invalidate]; - - // 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]; -} - -#pragma mark Bundle loading - -- (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]; + dispatch_async(dispatch_get_main_queue(), ^{ _registered = YES; - NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ - @"rootTag": self.reactTag, - @"initialProps": self.initialProperties ?: @{}, - }; + @"rootTag": _contentView.reactTag, + @"initialProps": self.initialProperties ?: @{}, + }; + [_bridge.uiManager registerRootView:_contentView]; [_bridge enqueueJSCall:@"AppRegistry.runApplication" args:@[moduleName, appParameters]]; - } -} - -- (void)loadBundle -{ - [self invalidate]; - - if (!_scriptURL) { - return; - } - - // Clean up - [self removeGestureRecognizer:_touchHandler]; - [_touchHandler invalidate]; - [_executor invalidate]; - [_bridge invalidate]; - - _registered = NO; - - // Choose local executor if specified, followed by global, followed by default - _executor = [[_executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class] alloc] init]; - - /** - * HACK(t6568049) Most of the properties passed into the bridge are not used - * right now but it'll be changed soon so it's here for convenience. - */ - _bridge = [[RCTBridge alloc] initWithBundlePath:_scriptURL.absoluteString - moduleProvider:_moduleProvider - launchOptions:_launchOptions]; - [_bridge setJavaScriptExecutor:_executor]; - - _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 React 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; - } - if (!_bridge.isValid) { - return; // Bridge was invalidated in the meanwhile - } - - // 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(), ^{ - if (_bridge.isValid) { - [self bundleFinishedLoading:_error]; - } - }); - }]; - - }]; - - [task resume]; -} - -- (void)setScriptURL:(NSURL *)scriptURL -{ - if ([_scriptURL isEqual:scriptURL]) { - return; - } - - _scriptURL = scriptURL; - [self loadBundle]; + }); } - (void)layoutSubviews { [super layoutSubviews]; + _contentView.frame = self.bounds; if (_registered) { - [_bridge.uiManager setFrame:self.frame forRootView:self]; + [_bridge.uiManager setFrame:self.frame forRootView:_contentView]; } } +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + _contentView.frame = self.bounds; +} + - (void)reload { - [self loadBundle]; + [self tearDown]; + [self setUp]; } + (void)reloadAll { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification + object:self]; +} + +- (NSNumber *)reactTag +{ + return _contentView.reactTag; } - (void)startOrResetInteractionTiming @@ -350,3 +229,14 @@ static Class _globalExecutorClass; } @end + +@implementation RCTUIManager (RCTRootView) + +- (NSNumber *)allocateRootTag +{ + NSNumber *rootTag = objc_getAssociatedObject(self, _cmd) ?: @1; + objc_setAssociatedObject(self, _cmd, @(rootTag.integerValue + 10), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + return rootTag; +} + +@end diff --git a/React/Executors/RCTContextExecutor.h b/React/Executors/RCTContextExecutor.h index 159965a2f..6e62d87b6 100644 --- a/React/Executors/RCTContextExecutor.h +++ b/React/Executors/RCTContextExecutor.h @@ -9,7 +9,7 @@ #import -#import "RCTJavaScriptExecutor.h" +#import "../Base/RCTJavaScriptExecutor.h" // TODO (#5906496): Might RCTJSCoreExecutor be a better name for this? diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index c70dda9c6..4f42cd0b7 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -30,8 +30,7 @@ @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. + * Register a root view with the RCTUIManager. */ - (void)registerRootView:(UIView *)rootView; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 82324f281..185982fdc 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -192,9 +192,10 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio NSMutableDictionary *_defaultShadowViews; // RCT thread only NSMutableDictionary *_defaultViews; // Main thread only NSDictionary *_viewManagers; + NSUInteger _rootTag; } -@synthesize bridge =_bridge; +@synthesize bridge = _bridge; /** * This function derives the view name automatically @@ -239,6 +240,7 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) // Internal resources _pendingUIBlocks = [[NSMutableArray alloc] init]; _rootViewTags = [[NSMutableSet alloc] init]; + _rootTag = 1; } return self; } @@ -259,6 +261,7 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) _bridge = bridge; _shadowQueue = _bridge.shadowQueue; + _shadowViewRegistry = [[RCTSparseArray alloc] init]; // Get view managers from bridge NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 537590ad1..f8dfd3862 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; }; 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+React.m */; }; + 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */; }; 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE21AAC4AE100FC20F4 /* RCTMap.m */; }; 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */; }; 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F362081AABD06A001CE568 /* RCTSwitch.m */; }; @@ -146,6 +147,8 @@ 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; 13EFFCCF1A98E6FE002607DC /* RCTJSMethodRegistrar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSMethodRegistrar.h; sourceTree = ""; }; + 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; + 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptLoader.m; sourceTree = ""; }; 14435CE11AAC4AE100FC20F4 /* RCTMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMap.h; sourceTree = ""; }; 14435CE21AAC4AE100FC20F4 /* RCTMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMap.m; sourceTree = ""; }; 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapManager.h; sourceTree = ""; }; @@ -348,6 +351,8 @@ 83CBBA491A601E3B00E9B192 /* Base */ = { isa = PBXGroup; children = ( + 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */, + 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */, 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */, 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */, 83CBBA5E1A601EAA00E9B192 /* RCTBridge.h */, @@ -473,6 +478,7 @@ 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */, 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, + 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */, 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */, 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */, 13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */,