diff --git a/Examples/UIExplorer/Navigator/JumpingNavSample.js b/Examples/UIExplorer/Navigator/JumpingNavSample.js index 7b0fa2122dd880..b38d9291d05d77 100644 --- a/Examples/UIExplorer/Navigator/JumpingNavSample.js +++ b/Examples/UIExplorer/Navigator/JumpingNavSample.js @@ -55,29 +55,25 @@ class JumpingNavBar extends React.Component { render() { return ( - + { this.props.onTabIndex(0); }} - children={} - /> + onPress={() => { this.props.onTabIndex(0); }}> + + { this.props.onTabIndex(1); }} - children={} - /> + onPress={() => { this.props.onTabIndex(1); }}> + + { this.props.onTabIndex(2); }} - children={} - /> + onPress={() => { this.props.onTabIndex(2); }}> + + ); diff --git a/Examples/UIExplorer/TabBarIOSExample.js b/Examples/UIExplorer/TabBarIOSExample.js index 815c07d894a8a2..a8f913a07de228 100644 --- a/Examples/UIExplorer/TabBarIOSExample.js +++ b/Examples/UIExplorer/TabBarIOSExample.js @@ -22,9 +22,8 @@ var { Text, View, } = React; -var TabBarItemIOS = TabBarIOS.Item; -var TabBarExample = React.createClass({ +var TabBarExample = React.createClass({ statics: { title: '', description: 'Tab-based navigation.' @@ -42,19 +41,16 @@ var TabBarExample = React.createClass({ return ( {pageText} - {this.state.presses} re-renders of this tab + {this.state.presses} re-renders of the More tab ); }, render: function() { return ( - - + { this.setState({ @@ -62,12 +58,10 @@ var TabBarExample = React.createClass({ }); }}> {this._renderContent('#414A8C', 'Blue Tab')} - - + 0 ? this.state.notifCount : undefined} selected={this.state.selectedTab === 'redTab'} onPress={() => { this.setState({ @@ -76,11 +70,9 @@ var TabBarExample = React.createClass({ }); }}> {this._renderContent('#783E33', 'Red Tab')} - - + { this.setState({ @@ -89,7 +81,7 @@ var TabBarExample = React.createClass({ }); }}> {this._renderContent('#21551C', 'Green Tab')} - + ); }, @@ -107,14 +99,4 @@ var styles = StyleSheet.create({ }, }); -// This is needed because the actual image may not exist as a file and -// is used by the native code to load a system image. -// TODO(nicklockwood): How can this fit our require system? -function _ix_DEPRECATED(imageUri) { - return { - uri: imageUri, - isStatic: true, - }; -} - module.exports = TabBarExample; diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index bb283e1cea68a4..009bbebb4571c4 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -357,7 +357,7 @@ var ScrollResponderMixin = { * A helper function to zoom to a specific rect in the scrollview. * @param {object} rect Should have shape {x, y, w, h} */ - scrollResponderZoomTo: function(rect: { x: number; y: number; w: number; h: number; }) { + scrollResponderZoomTo: function(rect: { x: number; y: number; width: number; height: number; }) { RCTUIManagerDeprecated.zoomToRect(this.getNodeHandle(), rect); }, diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js index e508268c57f57d..05ac37c74f5bc7 100644 --- a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js @@ -13,8 +13,9 @@ var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); -var TabBarItemIOS = require('TabBarItemIOS'); var StyleSheet = require('StyleSheet'); +var TabBarItemIOS = require('TabBarItemIOS'); +var View = require('View'); var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); @@ -22,6 +23,11 @@ var TabBarIOS = React.createClass({ statics: { Item: TabBarItemIOS, }, + + propTypes: { + style: View.propTypes.style, + }, + render: function() { return ( diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js index 11b02ab3964622..f795ed7ced6dee 100644 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js @@ -24,12 +24,57 @@ var merge = require('merge'); var TabBarItemIOS = React.createClass({ propTypes: { - icon: Image.propTypes.source.isRequired, - onPress: React.PropTypes.func.isRequired, - selected: React.PropTypes.bool.isRequired, - badgeValue: React.PropTypes.string, - title: React.PropTypes.string, + /** + * Little red bubble that sits at the top right of the icon. + */ + badge: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + ]), + /** + * Items comes with a few predefined system icons. Note that if you are + * using them, the title and selectedIcon will be overriden with the + * system ones. + */ + systemIcon: React.PropTypes.oneOf([ + 'bookmarks', + 'contacts', + 'downloads', + 'favorites', + 'featured', + 'history', + 'more', + 'most-recent', + 'most-viewed', + 'recents', + 'search', + 'top-rated', + ]), + /** + * A custom icon for the tab. It is ignored when a system icon is defined. + */ + icon: Image.propTypes.source, + /** + * A custom icon when the tab is selected. It is ignored when a system + * icon is defined. If left empty, the icon will be tinted in blue. + */ + selectedIcon: Image.propTypes.source, + /** + * Callback when this tab is being selected, you should change the state of your + * component to set selected={true}. + */ + onPress: React.PropTypes.func, + /** + * It specifies whether the children are visible or not. If you see a + * blank content, you probably forgot to add a selected one. + */ + selected: React.PropTypes.bool, style: View.propTypes.style, + /** + * Text that appears under the icon. It is ignored when a system icon + * is defined. + */ + title: React.PropTypes.string, }, getInitialState: function() { @@ -63,13 +108,21 @@ var TabBarItemIOS = React.createClass({ tabContents = ; } + var icon = this.props.systemIcon || ( + this.props.icon && this.props.icon.uri + ); + + var badge = typeof this.props.badge === 'number' ? + '' + this.props.badge : + this.props.badge; + return ( {tabContents} diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index 7dadeb5bba795c..d9118b7484f0e7 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -36,12 +36,12 @@ function handleException(e: Exception) { ); if (RCTExceptionsManager) { - RCTExceptionsManager.reportUnhandledException(e.message, format(stack)); + RCTExceptionsManager.reportUnhandledException(e.message, stack); if (__DEV__) { (sourceMapPromise = sourceMapPromise || loadSourceMap()) .then(map => { var prettyStack = parseErrorStack(e, map); - RCTExceptionsManager.updateExceptionMessage(e.message, format(prettyStack)); + RCTExceptionsManager.updateExceptionMessage(e.message, prettyStack); }) .then(null, error => { console.error('#CLOWNTOWN (error while displaying error): ' + error.message); @@ -71,13 +71,4 @@ function fillSpaces(n) { return new Array(n + 1).join(' '); } -// HACK(frantic) Android currently expects stack trace to be a string #5920439 -function format(stack) { - if (Platform.OS === 'android') { - return stackToString(stack); - } else { - return stack; - } -} - module.exports = { handleException }; diff --git a/Libraries/RCTTest/RCTTestModule.h b/Libraries/RCTTest/RCTTestModule.h index 0f5adcd2cdfd91..21ed60c6b91f05 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 27aac48d956abc..8cb5169c37a1be 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 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictiona 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 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictiona [[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 933c49aedf3d48..ddf973b24510ce 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -168,6 +168,7 @@ - (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)object - (void)invalidate { + [_jsQueue cancelAllOperations]; _socket.delegate = nil; [_socket closeWithCode:1000 reason:@"Invalidated"]; _socket = nil; diff --git a/Libraries/ReactIOS/ReactIOSDefaultInjection.js b/Libraries/ReactIOS/ReactIOSDefaultInjection.js index a642369735a725..7b3df0d8515a80 100644 --- a/Libraries/ReactIOS/ReactIOSDefaultInjection.js +++ b/Libraries/ReactIOS/ReactIOSDefaultInjection.js @@ -38,6 +38,7 @@ var ResponderEventPlugin = require('ResponderEventPlugin'); var UniversalWorkerNodeHandle = require('UniversalWorkerNodeHandle'); var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var invariant = require('invariant'); // Just to ensure this gets packaged, since its only caller is from Native. require('RCTEventEmitter'); @@ -94,6 +95,14 @@ function inject() { ReactNativeComponent.injection.injectTextComponentClass( ReactIOSTextComponent ); + ReactNativeComponent.injection.injectAutoWrapper(function(tag) { + // Show a nicer error message for non-function tags + var info = ''; + if (typeof tag === 'string' && /^[a-z]/.test(tag)) { + info += ' Each component name should start with an uppercase letter.'; + } + invariant(false, 'Expected a component class, got %s.%s', tag, info); + }); NodeHandle.injection.injectImplementation(UniversalWorkerNodeHandle); } diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 7aa28e49b0a5e0..05767533d59b7b 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 148a5d93320b1c..1ad3d58b75d914 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 @@ @implementation RCTModuleMethod NSString *_methodName; } +static Class _globalExecutorClass; + - (instancetype)initWithMethodName:(NSString *)methodName JSMethodName:(NSString *)JSMethodName { @@ -497,33 +506,41 @@ @implementation RCTBridge 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 +- (void)setUp { - _javaScriptExecutor = executor; + 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; - [self setUp]; -} + _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; + _shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL); -- (void)setUp -{ // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; for (id module in _moduleProvider ? _moduleProvider() : nil) { @@ -574,11 +591,73 @@ - (void)setUp 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 (_javaScriptExecutor == nil) { + /** + * HACK (tadeu): If it failed to connect to the debugger, set loaded to true so we can + * reload + */ + _loaded = YES; + } else 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 + // Workaround around the first cmd+r not working: http://openradar.appspot.com/19613391 + // You can register just the cmd key and do nothing. This will trigger the bug and cmd+r + // will work like a charm! + [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + // Do nothing + }]; + + [[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 +681,13 @@ - (BOOL)isValid - (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 +695,6 @@ - (void)invalidate [_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 +705,7 @@ - (void)invalidate // Release modules (breaks retain cycle if module has strong bridge reference) _modulesByID = nil; _modulesByName = nil; + _loaded = NO; } /** @@ -647,9 +729,11 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args 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 +877,19 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i 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/RCTConvert.m b/React/Base/RCTConvert.m index 3f8057d615c422..5fbc2d527b2cf0 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -225,6 +225,7 @@ + (type)type:(id)json \ NSString *key = aliases[alias]; \ NSNumber *number = json[key]; \ if (number) { \ + RCTLogWarn(@"Using deprecated '%@' property for '%s'. Use '%@' instead.", alias, #type, key); \ ((NSMutableDictionary *)json)[key] = number; \ } \ } \ @@ -245,9 +246,9 @@ + (type)type:(id)json \ } RCT_CUSTOM_CONVERTER(CGFloat, CGFloat, [self double:json]) -RCT_CGSTRUCT_CONVERTER(CGPoint, (@[@"x", @"y"]), nil) +RCT_CGSTRUCT_CONVERTER(CGPoint, (@[@"x", @"y"]), (@{@"l": @"x", @"t": @"y"})) RCT_CGSTRUCT_CONVERTER(CGSize, (@[@"width", @"height"]), (@{@"w": @"width", @"h": @"height"})) -RCT_CGSTRUCT_CONVERTER(CGRect, (@[@"x", @"y", @"width", @"height"]), (@{@"w": @"width", @"h": @"height"})) +RCT_CGSTRUCT_CONVERTER(CGRect, (@[@"x", @"y", @"width", @"height"]), (@{@"l": @"x", @"t": @"y", @"w": @"width", @"h": @"height"})) RCT_CGSTRUCT_CONVERTER(UIEdgeInsets, (@[@"top", @"left", @"bottom", @"right"]), nil) RCT_ENUM_CONVERTER(CGLineJoin, (@{ diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index e7d3b8b3010b3d..a49e076e621743 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 77ce7393529d91..5fe58f60866607 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 @@ - (void)show 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)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger - (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 @@ - (void)_checkForUpdates:(NSURL *)URL 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 00000000000000..7c750c5857336b --- /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 00000000000000..1d61946b974947 --- /dev/null +++ b/React/Base/RCTJavaScriptLoader.m @@ -0,0 +1,140 @@ +// 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 == nil) { + NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" + code:1 + userInfo:@{NSLocalizedDescriptionKey: @"No script URL provided"}]; + onComplete(error); + return; + } else if ([scriptURL isFileURL]) { + NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath]; + NSString *localPath = [scriptURL.absoluteString substringFromIndex:@"file://".length]; + + if (![localPath hasPrefix:bundlePath]) { + NSString *absolutePath = [NSString stringWithFormat:@"%@/%@", bundlePath, localPath]; + scriptURL = [NSURL fileURLWithPath:absolutePath]; + } + } + + 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; + } + 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 e5776cc6f4a171..b9e91b7a017b3e 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 ac39a9aed3a0cf..9d913c048ec895 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 @@ - (void)setJavaScriptExecutor:(id)executor; @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]; + RCTBridge *bridge = [[RCTBridge alloc] initWithBundlePath:bundleURL.absoluteString + moduleProvider:nil + launchOptions:launchOptions]; + return [self initWithBridge:bridge + moduleName:moduleName]; +} + +- (void)dealloc +{ + [self tearDown]; +} + +- (void)setUp +{ + 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]; + + [[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]; + } } - return self; } - /** - * HACK(t6568049) Private constructor for testing purposes - */ -- (instancetype)_initWithBundleURL:(NSURL *)bundleURL - moduleName:(NSString *)moduleName - launchOptions:(NSDictionary *)launchOptions - moduleProvider:(RCTBridgeModuleProviderBlock)moduleProvider +- (void)tearDown { - if ((self = [super init])) { - _moduleProvider = moduleProvider; - _moduleName = moduleName; - _launchOptions = launchOptions; - [self setUp]; - [self setScriptURL:bundleURL]; + if (_registered) { + _registered = NO; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_contentView removeGestureRecognizer:_touchHandler]; + [_contentView removeFromSuperview]; + [_touchHandler invalidate]; + [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" + args:@[_contentView.reactTag]]; } - return self; } -- (void)setUp +- (BOOL)isValid { - // 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; - - // Add reload observer - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reload) - name:RCTReloadNotification - object:nil]; + return _registered; +} + +- (void)invalidate +{ + [self tearDown]; +} + +- (UIViewController *)backingViewController { + return _backingViewController ?: [super backingViewController]; } - (BOOL)canBecomeFirstResponder @@ -135,7 +157,7 @@ - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (motion == UIEventSubtypeMotionShake && self.enableDevMenu) { if (!_devMenu) { - _devMenu = [[RCTDevMenu alloc] initWithRootView:self]; + _devMenu = [[RCTDevMenu alloc] initWithBridge:self.bridge]; } [_devMenu show]; } @@ -149,194 +171,51 @@ + (NSArray *)JSMethods ]; } -- (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 +- (void)layoutSubviews { - [self invalidate]; - - if (!_scriptURL) { - return; + [super layoutSubviews]; + _contentView.frame = self.bounds; + if (_registered) { + [_bridge.uiManager setFrame:self.frame forRootView:_contentView]; } - - // 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 +- (void)setFrame:(CGRect)frame { - if ([_scriptURL isEqual:scriptURL]) { - return; - } - - _scriptURL = scriptURL; - [self loadBundle]; + [super setFrame:frame]; + _contentView.frame = self.bounds; } -- (void)layoutSubviews +- (void)reload { - [super layoutSubviews]; - if (_registered) { - [_bridge.uiManager setFrame:self.frame forRootView:self]; - } + [self tearDown]; + [self setUp]; } -- (void)reload ++ (void)reloadAll { - [self loadBundle]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification + object:self]; } -+ (void)reloadAll +- (NSNumber *)reactTag { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; + return _contentView.reactTag; } - (void)startOrResetInteractionTiming @@ -350,3 +229,14 @@ - (NSDictionary *)endAndResetInteractionTiming } @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 159965a2fbed01..6e62d87b6db8c2 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 c70dda9c63f3de..4f42cd0b7ff33d 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 82324f28126095..185982fdcf205b 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -192,9 +192,10 @@ @implementation RCTUIManager 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 @@ - (instancetype)init // Internal resources _pendingUIBlocks = [[NSMutableArray alloc] init]; _rootViewTags = [[NSMutableSet alloc] init]; + _rootTag = 1; } return self; } @@ -259,6 +261,7 @@ - (void)setBridge:(RCTBridge *)bridge _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 537590ad119b8f..f8dfd38623b0d1 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 */, diff --git a/React/Views/RCTTabBarItem.m b/React/Views/RCTTabBarItem.m index 530fef893e0882..967ae04afc310e 100644 --- a/React/Views/RCTTabBarItem.m +++ b/React/Views/RCTTabBarItem.m @@ -31,19 +31,20 @@ - (void)setIcon:(NSString *)icon static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ systemIcons = @{ - @"more": @(UITabBarSystemItemMore), - @"favorites": @(UITabBarSystemItemFavorites), - @"featured": @(UITabBarSystemItemFeatured), - @"topRated": @(UITabBarSystemItemTopRated), - @"recents": @(UITabBarSystemItemRecents), - @"contacts": @(UITabBarSystemItemContacts), - @"history": @(UITabBarSystemItemHistory), - @"bookmarks": @(UITabBarSystemItemBookmarks), - @"search": @(UITabBarSystemItemSearch), - @"downloads": @(UITabBarSystemItemDownloads), - @"mostRecent": @(UITabBarSystemItemMostRecent), - @"mostViewed": @(UITabBarSystemItemMostViewed), - }; + @"bookmarks": @(UITabBarSystemItemBookmarks), + @"contacts": @(UITabBarSystemItemContacts), + @"downloads": @(UITabBarSystemItemDownloads), + @"favorites": @(UITabBarSystemItemFavorites), + @"featured": @(UITabBarSystemItemFeatured), + @"history": @(UITabBarSystemItemHistory), + @"more": @(UITabBarSystemItemMore), + @"most-recent": @(UITabBarSystemItemMostRecent), + @"most-viewed": @(UITabBarSystemItemMostViewed), + @"recents": @(UITabBarSystemItemRecents), + @"search": @(UITabBarSystemItemSearch), + @"top-rated": @(UITabBarSystemItemTopRated), + }; + }); // Update icon diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index febf56703ce5c9..d40798302b2a94 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -16,6 +16,20 @@ static const RCTBorderSide RCTBorderSideCount = 4; +static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) +{ + for (UIView *subview in [view.subviews reverseObjectEnumerator]) { + if (!subview.isHidden && subview.isUserInteractionEnabled && subview.alpha > 0) { + CGPoint convertedPoint = [subview convertPoint:point fromView:view]; + UIView *subviewHitTestView = [subview hitTest:convertedPoint withEvent:event]; + if (subviewHitTestView != nil) { + return subviewHitTestView; + } + } + } + return nil; +} + @implementation UIView (RCTViewUnmounting) - (void)react_remountAllSubviews @@ -120,20 +134,11 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event case RCTPointerEventsNone: return nil; case RCTPointerEventsUnspecified: - return [super hitTest:point withEvent:event]; + return RCTViewHitTest(self, point, event) ?: [super hitTest:point withEvent:event]; case RCTPointerEventsBoxOnly: return [super hitTest:point withEvent:event] ? self: nil; case RCTPointerEventsBoxNone: - for (UIView *subview in [self.subviews reverseObjectEnumerator]) { - if (!subview.isHidden && subview.isUserInteractionEnabled && subview.alpha > 0) { - CGPoint convertedPoint = [subview convertPoint:point fromView:self]; - UIView *subviewHitTestView = [subview hitTest:convertedPoint withEvent:event]; - if (subviewHitTestView != nil) { - return subviewHitTestView; - } - } - } - return nil; + return RCTViewHitTest(self, point, event); default: RCTLogError(@"Invalid pointer-events specified %zd on %@", _pointerEvents, self); return [super hitTest:point withEvent:event]; diff --git a/package.json b/package.json index be08b782b6cae7..274a9ffdd7a7b9 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,9 @@ }, "dependencies": { "connect": "2.8.3", - "jstransform": "10.0.1", + "jstransform": "10.1.0", "react-timer-mixin": "^0.13.1", - "react-tools": "0.13.0-rc2", + "react-tools": "0.13.1", "rebound": "^0.0.12", "source-map": "0.1.31", "stacktrace-parser": "0.1.1", diff --git a/packager/packager.js b/packager/packager.js index ad47d1d991f733..e2a32f2caaacda 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -10,6 +10,8 @@ var fs = require('fs'); var path = require('path'); +var exec = require('child_process').exec; +var http = require('http'); if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules'))) { console.log( @@ -21,11 +23,9 @@ if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules'))) { process.exit(); } -var exec = require('child_process').exec; +var connect = require('connect'); var ReactPackager = require('./react-packager'); var blacklist = require('./blacklist.js'); -var connect = require('connect'); -var http = require('http'); var launchEditor = require('./launchEditor.js'); var parseCommandLine = require('./parseCommandLine.js'); var webSocketProxy = require('./webSocketProxy.js'); diff --git a/packager/react-packager/src/FileWatcher/index.js b/packager/react-packager/src/FileWatcher/index.js index 6f451b482297f9..9af96c1442ce07 100644 --- a/packager/react-packager/src/FileWatcher/index.js +++ b/packager/react-packager/src/FileWatcher/index.js @@ -64,7 +64,10 @@ FileWatcher.prototype.end = function() { function createWatcher(rootConfig) { return detectingWatcherClass.then(function(Watcher) { - var watcher = new Watcher(rootConfig.dir, rootConfig.globs); + var watcher = new Watcher(rootConfig.dir, { + glob: rootConfig.globs, + dot: false, + }); return new Promise(function(resolve, reject) { var rejectTimeout = setTimeout(function() {