diff --git a/Examples/Movies/Movies/main.m b/Examples/Movies/Movies/main.m index 8954f343c..9c58a39a4 100644 --- a/Examples/Movies/Movies/main.m +++ b/Examples/Movies/Movies/main.m @@ -17,6 +17,6 @@ int main(int argc, char * argv[]) { @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } diff --git a/Examples/SampleApp/SampleAppTests/SampleAppTests.m b/Examples/SampleApp/SampleAppTests/SampleAppTests.m index 0fa28c29d..f06ffd148 100644 --- a/Examples/SampleApp/SampleAppTests/SampleAppTests.m +++ b/Examples/SampleApp/SampleAppTests/SampleAppTests.m @@ -58,7 +58,7 @@ } XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); - XCTAssertTrue(foundElement, @"Cound't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); + XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); } diff --git a/Examples/UIExplorer/CameraRollView.ios.js b/Examples/UIExplorer/CameraRollView.ios.js index 87dd23e6d..74507aa2c 100644 --- a/Examples/UIExplorer/CameraRollView.ios.js +++ b/Examples/UIExplorer/CameraRollView.ios.js @@ -59,6 +59,16 @@ var propTypes = { * imagesPerRow: Number of images to be shown in each row. */ imagesPerRow: React.PropTypes.number, + + /** + * The asset type, one of 'Photos', 'Videos' or 'All' + */ + assetType: React.PropTypes.oneOf([ + 'Photos', + 'Videos', + 'All', + ]), + }; var CameraRollView = React.createClass({ @@ -69,6 +79,7 @@ var CameraRollView = React.createClass({ groupTypes: 'SavedPhotos', batchSize: 5, imagesPerRow: 1, + assetType: 'Photos', renderImage: function(asset) { var imageSize = 150; var imageStyle = [styles.image, {width: imageSize, height: imageSize}]; @@ -89,6 +100,7 @@ var CameraRollView = React.createClass({ assets: ([]: Array), groupTypes: this.props.groupTypes, lastCursor: (null : ?string), + assetType: this.props.assetType, noMore: false, loadingMore: false, dataSource: ds, @@ -124,7 +136,8 @@ var CameraRollView = React.createClass({ var fetchParams: Object = { first: this.props.batchSize, - groupTypes: this.props.groupTypes + groupTypes: this.props.groupTypes, + assetType: this.props.assetType, }; if (this.state.lastCursor) { fetchParams.after = this.state.lastCursor; diff --git a/Examples/UIExplorer/NetInfoExample.js b/Examples/UIExplorer/NetInfoExample.js index c322a7432..6ab1805df 100644 --- a/Examples/UIExplorer/NetInfoExample.js +++ b/Examples/UIExplorer/NetInfoExample.js @@ -29,13 +29,13 @@ var ReachabilitySubscription = React.createClass({ }; }, componentDidMount: function() { - NetInfo.reachabilityIOS.addEventListener( + NetInfo.addEventListener( 'change', this._handleReachabilityChange ); }, componentWillUnmount: function() { - NetInfo.reachabilityIOS.removeEventListener( + NetInfo.removeEventListener( 'change', this._handleReachabilityChange ); @@ -63,16 +63,16 @@ var ReachabilityCurrent = React.createClass({ }; }, componentDidMount: function() { - NetInfo.reachabilityIOS.addEventListener( + NetInfo.addEventListener( 'change', this._handleReachabilityChange ); - NetInfo.reachabilityIOS.fetch().done( + NetInfo.fetch().done( (reachability) => { this.setState({reachability}); } ); }, componentWillUnmount: function() { - NetInfo.reachabilityIOS.removeEventListener( + NetInfo.removeEventListener( 'change', this._handleReachabilityChange ); diff --git a/Examples/UIExplorer/TransformExample.js b/Examples/UIExplorer/TransformExample.js new file mode 100644 index 000000000..a59a019b3 --- /dev/null +++ b/Examples/UIExplorer/TransformExample.js @@ -0,0 +1,159 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TransformExample + */ +'use strict'; + +var React = require('React'); + +var StyleSheet = require('StyleSheet'); +var TimerMixin = require('react-timer-mixin'); +var UIExplorerBlock = require('UIExplorerBlock'); +var UIExplorerPage = require('UIExplorerPage'); +var View = require('View'); + +var TransformExample = React.createClass({ + + mixins: [TimerMixin], + + getInitialState() { + return { + interval: this.setInterval(this._update, 800), + pulse: false, + }; + }, + + render() { + return ( + + + + + + + + + + + + + + ); + }, + + _update() { + this.setState({ + pulse: !this.state.pulse, + }); + }, + +}); + +var styles = StyleSheet.create({ + box1: { + left: 0, + backgroundColor: 'green', + height: 50, + position: 'absolute', + top: 0, + transform: [ + {translateX: 100}, + {translateY: 50}, + {rotate: '30deg'}, + {scaleX: 2}, + {scaleY: 2}, + ], + width: 50, + }, + box2: { + left: 0, + backgroundColor: 'purple', + height: 50, + position: 'absolute', + top: 0, + transform: [ + {scaleX: 2}, + {scaleY: 2}, + {translateX: 100}, + {translateY: 50}, + {rotate: '30deg'}, + ], + width: 50, + }, + box3step1: { + left: 0, + backgroundColor: '#ffb6c1', // lightpink + height: 50, + position: 'absolute', + top: 0, + transform: [ + {rotate: '30deg'}, + ], + width: 50, + }, + box3step2: { + left: 0, + backgroundColor: '#ff69b4', //hotpink + height: 50, + opacity: 0.5, + position: 'absolute', + top: 0, + transform: [ + {rotate: '30deg'}, + {scaleX: 2}, + {scaleY: 2}, + ], + width: 50, + }, + box3step3: { + left: 0, + backgroundColor: '#ff1493', // deeppink + height: 50, + opacity: 0.5, + position: 'absolute', + top: 0, + transform: [ + {rotate: '30deg'}, + {scaleX: 2}, + {scaleY: 2}, + {translateX: 100}, + {translateY: 50}, + ], + width: 50, + }, + box4: { + left: 0, + backgroundColor: '#ff8c00', // darkorange + height: 50, + position: 'absolute', + top: 0, + transform: [ + {translate: [200, 350]}, + {scale: 2.5}, + {rotate: '-0.2rad'}, + ], + width: 100, + }, + box5: { + backgroundColor: '#800000', // maroon + height: 50, + position: 'absolute', + right: 0, + top: 0, + width: 50, + }, + box5Transform: { + transform: [ + {translate: [-50, 35]}, + {rotate: '50deg'}, + {scale: 2}, + ], + }, +}); + + +module.exports = TransformExample; diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index fdbda4dc8..a030220ca 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -30,7 +30,7 @@ var { var { TestModule } = React.addons; var Settings = require('Settings'); -import type { Example, ExampleModule } from 'ExampleTypes'; +import type { ExampleModule } from 'ExampleTypes'; var createExamplePage = require('./createExamplePage'); @@ -154,7 +154,9 @@ class UIExplorerList extends React.Component { dataSource={this.state.dataSource} renderRow={this._renderRow.bind(this)} renderSectionHeader={this._renderSectionHeader} + keyboardShouldPersistTaps={true} automaticallyAdjustContentInsets={false} + keyboardDismissMode="onDrag" /> ); diff --git a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m index 45df4e417..d48b25ddf 100644 --- a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m +++ b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m @@ -62,6 +62,9 @@ // Make sure this test runs first because the other tests will tear out the rootView - (void)testAAA_RootViewLoadsAndRenders { + // TODO (t7296305) Fix and Re-Enable this UIExplorer Test + return; + UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController; RCTAssert([vc.view isKindOfClass:[RCTRootView class]], @"This test must run first."); NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; @@ -82,7 +85,7 @@ } XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); - XCTAssertTrue(foundElement, @"Cound't find element with '' text in %d seconds", TIMEOUT_SECONDS); + XCTAssertTrue(foundElement, @"Couldn't find element with '' text in %d seconds", TIMEOUT_SECONDS); } #define RCT_SNAPSHOT_TEST(name, reRecord) \ diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index 0d008ae75..67fa50830 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -29,8 +29,16 @@ var GROUP_TYPES_OPTIONS = [ 'SavedPhotos', // default ]; +var ASSET_TYPE_OPTIONS = [ + 'All', + 'Videos', + 'Photos', // default +]; + + // Flow treats Object and Array as disjoint types, currently. deepFreezeAndThrowOnMutationInDev((GROUP_TYPES_OPTIONS: any)); +deepFreezeAndThrowOnMutationInDev((ASSET_TYPE_OPTIONS: any)); /** * Shape of the param arg for the `getPhotos` function. @@ -58,6 +66,11 @@ var getPhotosParamChecker = createStrictShapeTypeChecker({ * titles. */ groupName: ReactPropTypes.string, + + /** + * Specifies filter on asset type + */ + assetType: ReactPropTypes.oneOf(ASSET_TYPE_OPTIONS), }); /** @@ -94,6 +107,7 @@ var getPhotosReturnChecker = createStrictShapeTypeChecker({ class CameraRoll { static GroupTypesOptions: Array; + static AssetTypeOptions: Array; /** * Saves the image with tag `tag` to the camera roll. * @@ -154,5 +168,6 @@ class CameraRoll { } CameraRoll.GroupTypesOptions = GROUP_TYPES_OPTIONS; +CameraRoll.AssetTypeOptions = ASSET_TYPE_OPTIONS; module.exports = CameraRoll; diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 17f1e477d..717f03847 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -45,7 +45,6 @@ var DEFAULT_INITIAL_ROWS = 10; var DEFAULT_SCROLL_RENDER_AHEAD = 1000; var DEFAULT_END_REACHED_THRESHOLD = 1000; var DEFAULT_SCROLL_CALLBACK_THROTTLE = 50; -var RENDER_INTERVAL = 20; var SCROLLVIEW_REF = 'listviewscroll'; @@ -258,7 +257,6 @@ var ListView = React.createClass({ // the component is laid out this.requestAnimationFrame(() => { this._measureAndUpdateScrollProps(); - this.setInterval(this._renderMoreRowsIfNeeded, RENDER_INTERVAL); }); }, @@ -329,7 +327,7 @@ var ListView = React.createClass({ totalIndex++; if (this.props.renderSeparator && - (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length -1)) { + (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)) { var adjacentRowHighlighted = this.state.highlightedRow.sectionID === sectionID && ( this.state.highlightedRow.rowID === rowID || @@ -397,6 +395,7 @@ var ListView = React.createClass({ _setScrollVisibleHeight: function(left, top, width, height) { this.scrollProperties.visibleHeight = height; this._updateVisibleRows(); + this._renderMoreRowsIfNeeded(); }, _renderMoreRowsIfNeeded: function() { @@ -443,8 +442,8 @@ var ListView = React.createClass({ } var updatedFrames = e && e.nativeEvent.updatedChildFrames; if (updatedFrames) { - updatedFrames.forEach((frame) => { - this._childFrames[frame.index] = merge(frame); + updatedFrames.forEach((newFrame) => { + this._childFrames[newFrame.index] = merge(newFrame); }); } var dataSource = this.props.dataSource; diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index f75430b77..56bd98c73 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -1290,7 +1290,7 @@ var Navigator = React.createClass({ key={this.state.idStack[i]} ref={'scene_' + i} onStartShouldSetResponderCapture={() => { - return i !== this.state.presentedIndex; + return !!this.state.transitionFromIndex || !!this.state.activeGesture; }} style={[styles.baseScene, this.props.sceneStyle, disabledSceneStyle]}> {React.cloneElement(child, { diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/Image/RCTCameraRollManager.m index 8e6c8a532..d7b42f885 100644 --- a/Libraries/Image/RCTCameraRollManager.m +++ b/Libraries/Image/RCTCameraRollManager.m @@ -69,7 +69,9 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params NSString *afterCursor = params[@"after"]; NSString *groupTypesStr = params[@"groupTypes"]; NSString *groupName = params[@"groupName"]; + NSString *assetType = params[@"assetType"]; ALAssetsGroupType groupTypes; + if ([groupTypesStr isEqualToString:@"Album"]) { groupTypes = ALAssetsGroupAlbum; } else if ([groupTypesStr isEqualToString:@"All"]) { @@ -93,7 +95,15 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params [[RCTImageLoader assetsLibrary] enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:ALAssetsGroupPropertyName]])) { - [group setAssetsFilter:ALAssetsFilter.allPhotos]; + + if (assetType == nil || [assetType isEqualToString:@"Photos"]) { + [group setAssetsFilter:ALAssetsFilter.allPhotos]; + } else if ([assetType isEqualToString:@"Videos"]) { + [group setAssetsFilter:ALAssetsFilter.allVideos]; + } else if ([assetType isEqualToString:@"All"]) { + [group setAssetsFilter:ALAssetsFilter.allAssets]; + } + [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) { if (result) { NSString *uri = [(NSURL *)[result valueForProperty:ALAssetPropertyAssetURL] absoluteString]; diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js index 866cf0368..5854dae0b 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -175,7 +175,7 @@ describe('resolveAssetSource', () => { isStatic: true, width: 100, height: 200, - uri: 'assets_awesomemodule_subdir_logo1_', + uri: 'awesomemodule_subdir_logo1_', }); }); }); diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js index 26592195d..301d70dd9 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/Libraries/Image/resolveAssetSource.js @@ -50,7 +50,8 @@ function getPathInArchive(asset) { return (assetDir + '/' + asset.name) .toLowerCase() .replace(/\//g, '_') // Encode folder structure in file name - .replace(/([^a-z0-9_])/g, ''); // Remove illegal chars + .replace(/([^a-z0-9_])/g, '') // Remove illegal chars + .replace(/^assets_/, ''); // Remove "assets_" prefix } else { // E.g. 'assets/AwesomeModule/icon@2x.png' return getScaledAssetPath(asset); diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index 224ce0966..ff2383a4c 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -30,17 +30,10 @@ function reportException(e: Exception, isFatal: bool, stack?: any) { if (!stack) { stack = parseErrorStack(e); } - if (!RCTExceptionsManager.reportFatalException || - !RCTExceptionsManager.reportSoftException) { - // Backwards compatibility - no differentiation - // TODO(#7049989): deprecate reportUnhandledException on Android - RCTExceptionsManager.reportUnhandledException(e.message, stack); + if (isFatal) { + RCTExceptionsManager.reportFatalException(e.message, stack); } else { - if (isFatal) { - RCTExceptionsManager.reportFatalException(e.message, stack); - } else { - RCTExceptionsManager.reportSoftException(e.message, stack); - } + RCTExceptionsManager.reportSoftException(e.message, stack); } if (__DEV__) { (sourceMapPromise = sourceMapPromise || loadSourceMap()) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 29d64152e..81978ee0c 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -33,7 +33,7 @@ if (typeof window === 'undefined') { window = GLOBAL; } -function handleErrorWithRedBox(e, isFatal) { +function handleError(e, isFatal) { try { require('ExceptionsManager').handleException(e, isFatal); } catch(ee) { @@ -43,7 +43,7 @@ function handleErrorWithRedBox(e, isFatal) { function setUpRedBoxErrorHandler() { var ErrorUtils = require('ErrorUtils'); - ErrorUtils.setGlobalHandler(handleErrorWithRedBox); + ErrorUtils.setGlobalHandler(handleError); } function setUpRedBoxConsoleErrorHandler() { diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index 2b65671a9..47184d181 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -12,8 +12,14 @@ 'use strict'; var NativeModules = require('NativeModules'); +var Platform = require('Platform'); var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTReachability = NativeModules.Reachability; + +if (Platform.OS === 'ios') { + var RCTNetInfo = NativeModules.Reachability; +} else if (Platform.OS === 'android') { + var RCTNetInfo = NativeModules.NetInfo; +} var DEVICE_REACHABILITY_EVENT = 'reachabilityDidChange'; @@ -28,11 +34,50 @@ type ReachabilityStateIOS = $Enum<{ wifi: string; }>; +type ConnectivityStateAndroid = $Enum<{ + NONE: string; + MOBILE: string; + WIFI: string; + MOBILE_MMS: string; + MOBILE_SUPL: string; + MOBILE_DUN: string; + MOBILE_HIPRI: string; + WIMAX: string; + BLUETOOTH: string; + DUMMY: string; + ETHERNET: string; + MOBILE_FOTA: string; + MOBILE_IMS: string; + MOBILE_CBS: string; + WIFI_P2P: string; + MOBILE_IA: string; + MOBILE_EMERGENCY: string; + PROXY: string; + VPN: string; + UNKNOWN: string; +}>; /** * NetInfo exposes info about online/offline status * - * ### reachabilityIOS + * ``` + * NetInfo.fetch().done((reach) => { + * console.log('Initial: ' + reach); + * }); + * function handleFirstConnectivityChange(reach) { + * console.log('First change: ' + reach); + * NetInfo.removeEventListener( + * 'change', + * handleFirstConnectivityChange + * ); + * } + * NetInfo.addEventListener( + * 'change', + * handleFirstConnectivityChange + * ); + * ``` + * + * ### IOS * * Asynchronously determine if the device is online and on a cellular network. * @@ -41,21 +86,35 @@ type ReachabilityStateIOS = $Enum<{ * - `cell` - device is connected via Edge, 3G, WiMax, or LTE * - `unknown` - error case and the network status is unknown * - * ``` - * NetInfo.reachabilityIOS.fetch().done((reach) => { - * console.log('Initial: ' + reach); + * ### Android + * + * Asynchronously determine if the device is connected and details about that connection. + * + * Android Connectivity Types + * - `NONE` - device is offline + * - `BLUETOOTH` - The Bluetooth data connection. + * - `DUMMY` - Dummy data connection. + * - `ETHERNET` - The Ethernet data connection. + * - `MOBILE` - The Mobile data connection. + * - `MOBILE_DUN` - A DUN-specific Mobile data connection. + * - `MOBILE_HIPRI` - A High Priority Mobile data connection. + * - `MOBILE_MMS` - An MMS-specific Mobile data connection. + * - `MOBILE_SUPL` - A SUPL-specific Mobile data connection. + * - `VPN` - A virtual network using one or more native bearers. Requires API Level 21 + * - `WIFI` - The WIFI data connection. + * - `WIMAX` - The WiMAX data connection. + * - `UNKNOWN` - Unknown data connection. + * The rest ConnectivityStates are hidden by the Android API, but can be used if necessary. + * + * ### isConnectionMetered + * + * Available on Android. Detect if the current active connection is metered or not. A network is + * classified as metered when the user is sensitive to heavy data usage on that connection due to + * monetary costs, data limitations or battery/performance issues. + * + * NetInfo.isConnectionMetered((isConnectionMetered) => { + * console.log('Connection is ' + (isConnectionMetered ? 'Metered' : 'Not Metered')); * }); - * function handleFirstReachabilityChange(reach) { - * console.log('First change: ' + reach); - * NetInfo.reachabilityIOS.removeEventListener( - * 'change', - * handleFirstReachabilityChange - * ); - * } - * NetInfo.reachabilityIOS.addEventListener( - * 'change', - * handleFirstReachabilityChange - * ); * ``` * * ### isConnected @@ -81,89 +140,101 @@ type ReachabilityStateIOS = $Enum<{ * ``` */ -var NetInfo = {}; +var _subscriptions = {}; -if (RCTReachability) { - - // RCTReachability is exposed, so this is an iOS-like environment and we will - // expose reachabilityIOS - - var _reachabilitySubscriptions = {}; - - NetInfo.reachabilityIOS = { - addEventListener: function ( - eventName: ChangeEventName, - handler: Function - ): void { - _reachabilitySubscriptions[handler] = RCTDeviceEventEmitter.addListener( - DEVICE_REACHABILITY_EVENT, - (appStateData) => { - handler(appStateData.network_reachability); - } - ); - }, - - removeEventListener: function( - eventName: ChangeEventName, - handler: Function - ): void { - if (!_reachabilitySubscriptions[handler]) { - return; +var NetInfo = { + addEventListener: function ( + eventName: ChangeEventName, + handler: Function + ): void { + _subscriptions[handler] = RCTDeviceEventEmitter.addListener( + DEVICE_REACHABILITY_EVENT, + (appStateData) => { + handler(appStateData.network_reachability); } - _reachabilitySubscriptions[handler].remove(); - _reachabilitySubscriptions[handler] = null; - }, + ); + }, - fetch: function(): Promise { - return new Promise((resolve, reject) => { - RCTReachability.getCurrentReachability( - function(resp) { - resolve(resp.network_reachability); - }, - reject - ); - }); - }, - }; + removeEventListener: function( + eventName: ChangeEventName, + handler: Function + ): void { + if (!_subscriptions[handler]) { + return; + } + _subscriptions[handler].remove(); + _subscriptions[handler] = null; + }, - var _isConnectedSubscriptions = {}; + fetch: function(): Promise { + return new Promise((resolve, reject) => { + RCTNetInfo.getCurrentReachability( + function(resp) { + resolve(resp.network_reachability); + }, + reject + ); + }); + }, - var _iosReachabilityIsConnected = function( + isConnected: {}, + + isConnectionMetered: {}, +}; + +if (Platform.OS === 'ios') { + var _isConnected = function( reachability: ReachabilityStateIOS ): bool { return reachability !== 'none' && reachability !== 'unknown'; }; +} else if (Platform.OS === 'android') { + var _isConnected = function( + connectionType: ConnectivityStateAndroid + ): bool { + return connectionType !== 'NONE' && connectionType !== 'UNKNOWN'; + }; +} - NetInfo.isConnected = { - addEventListener: function ( - eventName: ChangeEventName, - handler: Function - ): void { - _isConnectedSubscriptions[handler] = (reachability) => { - handler(_iosReachabilityIsConnected(reachability)); - }; - NetInfo.reachabilityIOS.addEventListener( - eventName, - _isConnectedSubscriptions[handler] - ); - }, +var _isConnectedSubscriptions = {}; - removeEventListener: function( - eventName: ChangeEventName, - handler: Function - ): void { - NetInfo.reachabilityIOS.removeEventListener( - eventName, - _isConnectedSubscriptions[handler] - ); - }, +NetInfo.isConnected = { + addEventListener: function ( + eventName: ChangeEventName, + handler: Function + ): void { + _isConnectedSubscriptions[handler] = (connection) => { + handler(_isConnected(connection)); + }; + NetInfo.addEventListener( + eventName, + _isConnectedSubscriptions[handler] + ); + }, - fetch: function(): Promise { - return NetInfo.reachabilityIOS.fetch().then( - (reachability) => _iosReachabilityIsConnected(reachability) - ); - }, + removeEventListener: function( + eventName: ChangeEventName, + handler: Function + ): void { + NetInfo.removeEventListener( + eventName, + _isConnectedSubscriptions[handler] + ); + }, + + fetch: function(): Promise { + return NetInfo.fetch().then( + (connection) => _isConnected(connection) + ); + }, +}; + +if (Platform.OS === 'android') { + NetInfo.isConnectionMetered = function(callback): void { + RCTNetInfo.isConnectionMetered((_isMetered) => { + callback(_isMetered); + }); }; } diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m index 1d0a793de..f4497a187 100644 --- a/Libraries/Network/RCTDataManager.m +++ b/Libraries/Network/RCTDataManager.m @@ -24,7 +24,6 @@ RCT_EXPORT_MODULE() */ RCT_EXPORT_METHOD(queryData:(NSString *)queryType withQuery:(NSDictionary *)query - queryHash:(__unused NSString *)queryHash responseSender:(RCTResponseSenderBlock)responseSender) { if ([queryType isEqualToString:@"http"]) { @@ -39,34 +38,35 @@ RCT_EXPORT_METHOD(queryData:(NSString *)queryType // Build data task NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *connectionError) { + NSHTTPURLResponse *httpResponse = nil; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + // Might be a local file request + httpResponse = (NSHTTPURLResponse *)response; + } + // Build response - NSDictionary *responseJSON; + NSArray *responseJSON; if (connectionError == nil) { NSStringEncoding encoding = NSUTF8StringEncoding; if (response.textEncodingName) { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } - NSHTTPURLResponse *httpResponse = nil; - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { - // Might be a local file request - httpResponse = (NSHTTPURLResponse *)response; - } - responseJSON = @{ - @"status": @([httpResponse statusCode] ?: 200), - @"responseHeaders": [httpResponse allHeaderFields] ?: @{}, - @"responseText": [[NSString alloc] initWithData:data encoding:encoding] ?: @"" - }; + responseJSON = @[ + @(httpResponse.statusCode ?: 200), + httpResponse.allHeaderFields ?: @{}, + [[NSString alloc] initWithData:data encoding:encoding] ?: @"", + ]; } else { - responseJSON = @{ - @"status": @0, - @"responseHeaders": @{}, - @"responseText": [connectionError localizedDescription] ?: [NSNull null] - }; + responseJSON = @[ + @(httpResponse.statusCode), + httpResponse.allHeaderFields ?: @{}, + connectionError.localizedDescription ?: [NSNull null], + ]; } // Send response (won't be sent on same thread as caller) - responseSender(@[RCTJSONStringify(responseJSON, NULL)]); + responseSender(responseJSON); }]; diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index 6c7367c18..9249047da 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -13,8 +13,6 @@ var RCTDataManager = require('NativeModules').DataManager; -var crc32 = require('crc32'); - var XMLHttpRequestBase = require('XMLHttpRequestBase'); class XMLHttpRequest extends XMLHttpRequestBase { @@ -28,12 +26,7 @@ class XMLHttpRequest extends XMLHttpRequestBase { data: data, headers: headers, }, - // TODO: Do we need this? is it used anywhere? - 'h' + crc32(method + '|' + url + '|' + data), - (result) => { - result = JSON.parse(result); - this.callback(result.status, result.responseHeaders, result.responseText); - } + this.callback.bind(this) ); } diff --git a/Libraries/ReactIOS/InspectorOverlay.js b/Libraries/ReactIOS/InspectorOverlay.js index 8b5c6c0cb..eeb6e7965 100644 --- a/Libraries/ReactIOS/InspectorOverlay.js +++ b/Libraries/ReactIOS/InspectorOverlay.js @@ -59,7 +59,7 @@ var InspectorOverlay = React.createClass({ ? 'flex-start' : 'flex-end'; - content.push(); + content.push(); content.push(); } return ( diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h index 189bff79e..d156bb4d6 100644 --- a/Libraries/Text/RCTShadowText.h +++ b/Libraries/Text/RCTShadowText.h @@ -28,7 +28,6 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, strong) UIColor *textBackgroundColor; @property (nonatomic, assign) NSWritingDirection writingDirection; -- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width; - (void)recomputeText; @end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 7e1daf908..511697f89 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -12,6 +12,8 @@ #import "RCTConvert.h" #import "RCTLog.h" #import "RCTShadowRawText.h" +#import "RCTSparseArray.h" +#import "RCTText.h" #import "RCTUtils.h" NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; @@ -19,6 +21,8 @@ NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; @implementation RCTShadowText { + NSTextStorage *_cachedTextStorage; + CGFloat _cachedTextStorageWidth; NSAttributedString *_cachedAttributedString; CGFloat _effectiveLetterSpacing; } @@ -50,8 +54,35 @@ static css_dim_t RCTMeasure(void *context, float width) return self; } +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties +{ + parentProperties = [super processUpdatedProperties:applierBlocks + parentProperties:parentProperties]; + + NSTextStorage *textStorage = [self buildTextStorageForWidth:self.frame.size.width]; + [applierBlocks addObject:^(RCTSparseArray *viewRegistry) { + RCTText *view = viewRegistry[self.reactTag]; + view.textStorage = textStorage; + }]; + + return parentProperties; +} + +- (void)applyLayoutNode:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition +{ + [super applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; + [self dirtyPropagation]; +} + - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width { + if (_cachedTextStorage && width == _cachedTextStorageWidth) { + return _cachedTextStorage; + } + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedString]; @@ -69,13 +100,23 @@ static css_dim_t RCTMeasure(void *context, float width) [layoutManager addTextContainer:textContainer]; [layoutManager ensureLayoutForTextContainer:textContainer]; + _cachedTextStorage = textStorage; + _cachedTextStorageWidth = width; + return textStorage; } +- (void)dirtyText +{ + [super dirtyText]; + _cachedTextStorage = nil; +} + - (void)recomputeText { [self attributedString]; [self setTextComputed]; + [self dirtyPropagation]; } - (NSAttributedString *)attributedString diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index d9e547c77..26c6329e2 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -96,12 +96,10 @@ RCT_EXPORT_SHADOW_PROPERTY(numberOfLines, NSUInteger) { NSNumber *reactTag = shadowView.reactTag; UIEdgeInsets padding = shadowView.paddingAsInsets; - NSTextStorage *textStorage = [shadowView buildTextStorageForWidth:shadowView.frame.size.width]; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTText *text = viewRegistry[reactTag]; text.contentInset = padding; - text.textStorage = textStorage; }; } diff --git a/Libraries/Utilities/BridgeProfiling.js b/Libraries/Utilities/BridgeProfiling.js new file mode 100644 index 000000000..e3c47907b --- /dev/null +++ b/Libraries/Utilities/BridgeProfiling.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule BridgeProfiling + * @flow + */ +'use strict'; + +var GLOBAL = GLOBAL || this; + +var BridgeProfiling = { + profile(profileName: String, args?: any) { + if (GLOBAL.__BridgeProfilingIsProfiling) { + if (args) { + try { + args = JSON.stringify(args); + } catch(err) { + args = err.message; + } + } + console.profile(profileName, args); + } + }, + + profileEnd() { + if (GLOBAL.__BridgeProfilingIsProfiling) { + console.profileEnd(); + } + }, +}; + +module.exports = BridgeProfiling; diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js index b93000a33..31fa0d4dc 100644 --- a/Libraries/Utilities/Dimensions.js +++ b/Libraries/Utilities/Dimensions.js @@ -29,6 +29,7 @@ if (dimensions && dimensions.windowPhysicalPixels) { width: windowPhysicalPixels.width / windowPhysicalPixels.scale, height: windowPhysicalPixels.height / windowPhysicalPixels.scale, scale: windowPhysicalPixels.scale, + fontScale: windowPhysicalPixels.fontScale, }; // delete so no callers rely on this existing diff --git a/Libraries/Utilities/ErrorUtils.js b/Libraries/Utilities/ErrorUtils.js index b66b08546..3f15ce2ce 100644 --- a/Libraries/Utilities/ErrorUtils.js +++ b/Libraries/Utilities/ErrorUtils.js @@ -22,6 +22,6 @@ var GLOBAL = this; * * However, we still want to treat ErrorUtils as a module so that other modules * that use it aren't just using a global variable, so simply export the global - * variable here. ErrorUtils is original defined in a file named error-guard.js. + * variable here. ErrorUtils is originally defined in a file named error-guard.js. */ module.exports = GLOBAL.ErrorUtils; diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index df34dde06..a09bd4f4a 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -17,6 +17,7 @@ var ReactUpdates = require('ReactUpdates'); var invariant = require('invariant'); var warning = require('warning'); +var BridgeProfiling = require('BridgeProfiling'); var JSTimersExecution = require('JSTimersExecution'); var INTERNAL_ERROR = 'Error in MessageQueue implementation'; @@ -277,7 +278,9 @@ var MessageQueueMixin = { if (DEBUG_SPY_MODE) { console.log('N->JS: Callback#' + cbID + '(' + JSON.stringify(args) + ')'); } + BridgeProfiling.profile('Callback#' + cbID + '(' + JSON.stringify(args) + ')'); cb.apply(scope, args); + BridgeProfiling.profileEnd(); } catch(ie_requires_catch) { throw ie_requires_catch; } finally { @@ -311,7 +314,9 @@ var MessageQueueMixin = { 'N->JS: ' + moduleName + '.' + methodName + '(' + JSON.stringify(params) + ')'); } + BridgeProfiling.profile(moduleName + '.' + methodName + '(' + JSON.stringify(params) + ')'); var ret = jsCall(this._requireFunc(moduleName), methodName, params); + BridgeProfiling.profileEnd(); return ret; }, @@ -330,7 +335,8 @@ var MessageQueueMixin = { processBatch: function(batch) { var self = this; - return guardReturn(function () { + BridgeProfiling.profile('MessageQueue.processBatch()'); + var flushedQueue = guardReturn(function () { ReactUpdates.batchedUpdates(function() { batch.forEach(function(call) { invariant( @@ -346,8 +352,12 @@ var MessageQueueMixin = { 'Unrecognized method called on BatchedBridge: ' + call.method); } }); + BridgeProfiling.profile('React.batchedUpdates()'); }); + BridgeProfiling.profileEnd(); }, null, this._flushedQueueUnguarded, this); + BridgeProfiling.profileEnd(); + return flushedQueue; }, setLoggingEnabled: function(enabled) { @@ -472,8 +482,10 @@ var MessageQueueMixin = { }, _flushedQueueUnguarded: function() { - // Call the functions registred via setImmediate + BridgeProfiling.profile('JSTimersExecution.callImmediates()'); + // Call the functions registered via setImmediate JSTimersExecution.callImmediates(); + BridgeProfiling.profileEnd(); var currentOutgoingItems = this._outgoingItems; this._swapAndReinitializeBuffer(); diff --git a/Libraries/Utilities/PixelRatio.js b/Libraries/Utilities/PixelRatio.js index a3e4d9e77..7660fad30 100644 --- a/Libraries/Utilities/PixelRatio.js +++ b/Libraries/Utilities/PixelRatio.js @@ -59,6 +59,20 @@ class PixelRatio { return Dimensions.get('window').scale; } + /** + * Returns the scaling factor for font sizes. This is the ratio that is used to calculate the + * absolute font size, so any elements that heavily depend on that should use this to do + * calculations. + * + * If a font scale is not set, this returns the device pixel ratio. + * + * Currently this is only implemented on Android and reflects the user preference set in + * Settings > Display > Font size, on iOS it will always return the default pixel ratio. + */ + static getFontScale(): number { + return Dimensions.get('window').fontScale || PixelRatio.get(); + } + /** * Converts a layout size (dp) to pixel size (px). * diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 47c7f5942..70868f721 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1339,8 +1339,11 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin * AnyThread */ + RCTProfileBeginFlowEvent(); + __weak RCTBatchedBridge *weakSelf = self; [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + RCTProfileEndFlowEvent(); RCTProfileBeginEvent(); RCTBatchedBridge *strongSelf = weakSelf; @@ -1348,13 +1351,17 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin return; } - id call = @{ - @"module": module, - @"method": method, - @"args": args, - @"context": context ?: @0, - }; + RCT_IF_DEV(NSNumber *callID = _RCTProfileBeginFlowEvent();) + id call = @{ + @"js_args": @{ + @"module": module, + @"method": method, + @"args": args, + }, + @"context": context ?: @0, + RCT_IF_DEV(@"call_id": callID,) + }; if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { strongSelf->_scheduledCallbacks[args[0]] = call; } else { @@ -1490,8 +1497,10 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin return NO; } + RCTProfileBeginFlowEvent(); __weak RCTBatchedBridge *weakSelf = self; [self dispatchBlock:^{ + RCTProfileEndFlowEvent(); RCTProfileBeginEvent(); RCTBatchedBridge *strongSelf = weakSelf; @@ -1525,16 +1534,18 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin - (void)_jsThreadUpdate:(CADisplayLink *)displayLink { RCTAssertJSThread(); - - RCTProfileImmediateEvent(@"JS Thread Tick", displayLink.timestamp, @"g"); - RCTProfileBeginEvent(); RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; for (id observer in _frameUpdateObservers) { if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { + RCT_IF_DEV(NSString *name = [NSString stringWithFormat:@"[%@ didUpdateFrame:%f]", observer, displayLink.timestamp];) + RCTProfileBeginFlowEvent(); [self dispatchBlock:^{ + RCTProfileEndFlowEvent(); + RCTProfileBeginEvent(); [observer didUpdateFrame:frameUpdate]; + RCTProfileEndEvent(name, @"objc_call,fps", nil); } forModule:RCTModuleIDsByName[RCTBridgeModuleNameForClass([observer class])]]; } } @@ -1544,18 +1555,29 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin calls = [calls filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSDictionary *call, NSDictionary *bindings) { return [call[@"context"] isEqualToNumber:currentExecutorID]; }]]; + + RCT_IF_DEV( + RCTProfileImmediateEvent(@"JS Thread Tick", displayLink.timestamp, @"g"); + + for (NSDictionary *call in calls) { + _RCTProfileEndFlowEvent(call[@"call_id"]); + } + ) + if (calls.count > 0) { _scheduledCalls = [[NSMutableArray alloc] init]; _scheduledCallbacks = [[RCTSparseArray alloc] init]; [self _actuallyInvokeAndProcessModule:@"BatchedBridge" method:@"processBatch" - arguments:@[calls] + arguments:@[[calls valueForKey:@"js_args"]] context:RCTGetExecutorID(_javaScriptExecutor)]; } RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); - [self.perfStats.jsGraph tick:displayLink.timestamp]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.perfStats.jsGraph tick:displayLink.timestamp]; + }); } - (void)_mainThreadUpdate:(CADisplayLink *)displayLink diff --git a/React/Base/RCTDefines.h b/React/Base/RCTDefines.h index 71550a30d..0d985db8a 100644 --- a/React/Base/RCTDefines.h +++ b/React/Base/RCTDefines.h @@ -42,6 +42,12 @@ #endif #endif +#if RCT_DEV +#define RCT_IF_DEV(...) __VA_ARGS__ +#else +#define RCT_IF_DEV(...) +#endif + /** * By default, only raise an NSAssertion in debug mode * (custom assert functions will still be called). diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index e6ed698d9..2009df542 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -11,11 +11,10 @@ #import "RCTAssert.h" #import "RCTBridge.h" -#import "RCTSparseArray.h" -static uint64_t RCTGetEventID(id event) +static NSNumber *RCTGetEventID(id event) { - return ( + return @( [event.viewTag intValue] | (((uint64_t)event.eventName.hash & 0xFFFF) << 32) | (((uint64_t)event.coalescingKey) << 48) @@ -68,7 +67,7 @@ static uint64_t RCTGetEventID(id event) @implementation RCTEventDispatcher { - RCTSparseArray *_eventQueue; + NSMutableDictionary *_eventQueue; NSLock *_eventQueueLock; } @@ -79,7 +78,7 @@ RCT_EXPORT_MODULE() - (instancetype)init { if ((self = [super init])) { - _eventQueue = [[RCTSparseArray alloc] init]; + _eventQueue = [[NSMutableDictionary alloc] init]; _eventQueueLock = [[NSLock alloc] init]; } return self; @@ -139,7 +138,7 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveEvent); [_eventQueueLock lock]; - uint64_t eventID = RCTGetEventID(event); + NSNumber *eventID = RCTGetEventID(event); id previousEvent = _eventQueue[eventID]; if (previousEvent) { @@ -176,14 +175,14 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveEvent); - (void)didUpdateFrame:(RCTFrameUpdate *)update { - RCTSparseArray *eventQueue; + NSDictionary *eventQueue; [_eventQueueLock lock]; eventQueue = _eventQueue; - _eventQueue = [[RCTSparseArray alloc] init]; + _eventQueue = [[NSMutableDictionary alloc] init]; [_eventQueueLock unlock]; - for (id event in eventQueue.allObjects) { + for (id event in eventQueue.allValues) { [self dispatchEvent:event]; } } diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h index 0c254c80a..f722b0a02 100644 --- a/React/Base/RCTProfile.h +++ b/React/Base/RCTProfile.h @@ -20,8 +20,23 @@ * before before using it. */ +NSString *const RCTProfileDidStartProfiling; +NSString *const RCTProfileDidEndProfiling; + #if RCT_DEV +#define RCTProfileBeginFlowEvent() \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ +NSNumber *__rct_profile_flow_id = _RCTProfileBeginFlowEvent(); \ +_Pragma("clang diagnostic pop") + +#define RCTProfileEndFlowEvent() \ +_RCTProfileEndFlowEvent(__rct_profile_flow_id) + +RCT_EXTERN NSNumber *_RCTProfileBeginFlowEvent(void); +RCT_EXTERN void _RCTProfileEndFlowEvent(NSNumber *); + /** * Returns YES if the profiling information is currently being collected */ @@ -88,6 +103,12 @@ RCT_EXTERN void RCTProfileImmediateEvent(NSString *, NSTimeInterval , NSString * #else +#define RCTProfileBeginFlowEvent() +#define _RCTProfileBeginFlowEvent() @0 + +#define RCTProfileEndFlowEvent() +#define _RCTProfileEndFlowEvent() + #define RCTProfileIsProfiling(...) NO #define RCTProfileInit(...) #define RCTProfileEnd(...) @"" diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m index 29e606e86..09989d1cb 100644 --- a/React/Base/RCTProfile.m +++ b/React/Base/RCTProfile.m @@ -17,6 +17,9 @@ #import "RCTDefines.h" #import "RCTUtils.h" +NSString *const RCTProfileDidStartProfiling = @"RCTProfileDidStartProfiling"; +NSString *const RCTProfileDidEndProfiling = @"RCTProfileDidEndProfiling"; + #if RCT_DEV #pragma mark - Prototypes @@ -113,10 +116,16 @@ void RCTProfileInit(void) RCTProfileSamples: [[NSMutableArray alloc] init], }; ); + + [[NSNotificationCenter defaultCenter] postNotificationName:RCTProfileDidStartProfiling + object:nil]; } NSString *RCTProfileEnd(void) { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTProfileDidEndProfiling + object:nil]; + RCTProfileLock( NSString *log = RCTJSONStringify(RCTProfileInfo, NULL); RCTProfileEventID = 0; @@ -171,4 +180,32 @@ void RCTProfileImmediateEvent(NSString *name, NSTimeInterval timestamp, NSString ); } +NSNumber *_RCTProfileBeginFlowEvent(void) +{ + static NSUInteger flowID = 0; + + CHECK(@0); + RCTProfileAddEvent(RCTProfileTraceEvents, + @"name": @"flow", + @"id": @(++flowID), + @"cat": @"flow", + @"ph": @"s", + @"ts": RCTProfileTimestamp(CACurrentMediaTime()), + ); + + return @(flowID); +} + +void _RCTProfileEndFlowEvent(NSNumber *flowID) +{ + CHECK(); + RCTProfileAddEvent(RCTProfileTraceEvents, + @"name": @"flow", + @"id": flowID, + @"cat": @"flow", + @"ph": @"f", + @"ts": RCTProfileTimestamp(CACurrentMediaTime()), + ); +} + #endif diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 3a2208739..627255c37 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -133,22 +133,27 @@ static JSValueRef RCTConsoleProfile(JSContextRef context, JSObjectRef object, JS profileName = [NSString stringWithFormat:@"Profile %d", profileCounter++]; } - [profiles addObjectsFromArray:@[profileName, profileID]]; + id profileInfo = [NSNull null]; + if (argumentCount > 1 && !JSValueIsUndefined(context, arguments[1])) { + profileInfo = @[RCTJSValueToNSString(context, arguments[1])]; + } + + [profiles addObjectsFromArray:@[profileName, profileID, profileInfo]]; - RCTLog(@"Profile '%@' finished.", profileName); return JSValueMakeUndefined(context); } static JSValueRef RCTConsoleProfileEnd(JSContextRef context, JSObjectRef object, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) { + NSString *profileInfo = [profiles lastObject]; + [profiles removeLastObject]; NSNumber *profileID = [profiles lastObject]; [profiles removeLastObject]; NSString *profileName = [profiles lastObject]; [profiles removeLastObject]; - _RCTProfileEndEvent(profileID, profileName, @"console", nil); + _RCTProfileEndEvent(profileID, profileName, @"console", profileInfo); - RCTLog(@"Profile '%@' started.", profileName); return JSValueMakeUndefined(context); } @@ -244,6 +249,13 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) #if RCT_DEV [strongSelf _addNativeHook:RCTConsoleProfile withName:"consoleProfile"]; [strongSelf _addNativeHook:RCTConsoleProfileEnd withName:"consoleProfileEnd"]; + + for (NSString *event in @[RCTProfileDidStartProfiling, RCTProfileDidEndProfiling]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(toggleProfilingFlag:) + name:event + object:nil]; + } #endif }]; @@ -252,6 +264,21 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) return self; } +- (void)toggleProfilingFlag:(NSNotification *)notification +{ + JSObjectRef globalObject = JSContextGetGlobalObject(_context.ctx); + + bool enabled = [notification.name isEqualToString:RCTProfileDidStartProfiling]; + JSStringRef JSName = JSStringCreateWithUTF8CString("__BridgeProfilingIsProfiling"); + JSObjectSetProperty(_context.ctx, + globalObject, + JSName, + JSValueMakeBoolean(_context.ctx, enabled), + kJSPropertyAttributeNone, + NULL); + JSStringRelease(JSName); +} + - (void)_addNativeHook:(JSObjectCallAsFunctionCallback)hook withName:(const char *)name { JSObjectRef globalObject = JSContextGetGlobalObject(_context.ctx); @@ -269,6 +296,10 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) - (void)invalidate { +#if RCT_DEV + [[NSNotificationCenter defaultCenter] removeObserver:self]; +#endif + [_context performSelector:@selector(invalidate) onThread:_javaScriptThread withObject:nil waitUntilDone:NO]; } diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index 7512c540d..64a5c85f0 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -44,8 +44,7 @@ RCT_EXPORT_METHOD(reportSoftException:(NSString *)message [_delegate handleSoftJSExceptionWithMessage:message stack:stack]; return; } - - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + // JS already logs the error via console. } RCT_EXPORT_METHOD(reportFatalException:(NSString *)message diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 127cbd9fc..cc580e903 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -481,6 +481,10 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa shadowView.newView = NO; } + // These are blocks to be executed on each view, immediately after + // reactSetFrame: has been called. Note that if reactSetFrame: is not called, + // these won't be called either, so this is not a suitable place to update + // properties that aren't related to layout. NSMutableArray *updateBlocks = [[NSMutableArray alloc] init]; for (RCTShadowView *shadowView in viewsWithNewFrames) { RCTViewManager *manager = _viewManagerRegistry[shadowView.reactTag]; @@ -917,6 +921,7 @@ RCT_EXPORT_METHOD(findSubviewIn:(NSNumber *)reactTag atPoint:(CGPoint)point call - (void)batchDidComplete { + RCTProfileBeginEvent(); // Gather blocks to be executed now that all view hierarchy manipulations have // been completed (note that these may still take place before layout has finished) for (RCTViewManager *manager in _viewManagers.allValues) { @@ -947,6 +952,9 @@ RCT_EXPORT_METHOD(findSubviewIn:(NSNumber *)reactTag atPoint:(CGPoint)point call _nextLayoutAnimation = nil; } + RCTProfileEndEvent(@"[RCTUIManager batchDidComplete]", @"uimanager", @{ + @"view_count": @([_viewRegistry count]), + }); [self flushUIBlocks]; } diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index c2e10750d..1c44033f6 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -20,8 +20,7 @@ typedef NS_ENUM(NSUInteger, RCTUpdateLifecycle) { RCTUpdateLifecycleDirtied, }; -// TODO: is this redundact now? -typedef void (^RCTApplierBlock)(RCTSparseArray *); +typedef void (^RCTApplierBlock)(RCTSparseArray *viewRegistry); /** * ShadowView tree mirrors RCT view tree. Every node is highly stateful. @@ -117,34 +116,48 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); * The applierBlocks set contains RCTApplierBlock functions that must be applied * on the main thread in order to update the view. */ -- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties; +- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties; + +/** + * Process the updated properties and apply them to view. Shadow view classes + * that add additional propagating properties should override this method. + */ +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties NS_REQUIRES_SUPER; /** * Calculate all views whose frame needs updating after layout has been calculated. * The viewsWithNewFrame set contains the reactTags of the views that need updating. */ -- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame parentConstraint:(CGSize)parentConstraint; +- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame + parentConstraint:(CGSize)parentConstraint; + +/** + * Recursively apply layout to children. + */ +- (void)applyLayoutNode:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER; /** * The following are implementation details exposed to subclasses. Do not call them directly */ -- (void)fillCSSNode:(css_node_t *)node; -- (void)dirtyLayout; +- (void)fillCSSNode:(css_node_t *)node NS_REQUIRES_SUPER; +- (void)dirtyLayout NS_REQUIRES_SUPER; - (BOOL)isLayoutDirty; -// TODO: is this still needed? -- (void)dirtyPropagation; +- (void)dirtyPropagation NS_REQUIRES_SUPER; - (BOOL)isPropagationDirty; -// TODO: move this to text node? -- (void)dirtyText; +- (void)dirtyText NS_REQUIRES_SUPER; +- (void)setTextComputed NS_REQUIRES_SUPER; - (BOOL)isTextDirty; -- (void)setTextComputed; /** * Triggers a recalculation of the shadow view's layout. */ -- (void)updateLayout; +- (void)updateLayout NS_REQUIRES_SUPER; /** * Computes the recursive offset, meaning the sum of all descendant offsets - diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index b26193f24..ba70ca34f 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -120,7 +120,9 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st // width = 213.5 - 106.5 = 107 // You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. -- (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition +- (void)applyLayoutNode:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition { if (!node->layout.should_update) { return; @@ -161,12 +163,19 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st for (int i = 0; i < node->children_count; ++i) { RCTShadowView *child = (RCTShadowView *)_reactSubviews[i]; - [child applyLayoutNode:node->get_child(node->context, i) viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; + [child applyLayoutNode:node->get_child(node->context, i) + viewsWithNewFrame:viewsWithNewFrame + absolutePosition:absolutePosition]; } } -- (NSDictionary *)processBackgroundColor:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties { + // TODO: we always refresh all propagated properties when propagation is + // dirtied, but really we should track which properties have changed and + // only update those. + if (!_backgroundColor) { UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp]; if (parentBackgroundColor) { @@ -190,14 +199,15 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st return parentProperties; } -- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties +- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties { if (_propagationLifecycle == RCTUpdateLifecycleComputed && [parentProperties isEqualToDictionary:_lastParentProperties]) { return; } _propagationLifecycle = RCTUpdateLifecycleComputed; _lastParentProperties = parentProperties; - NSDictionary *nextProps = [self processBackgroundColor:applierBlocks parentProperties:parentProperties]; + NSDictionary *nextProps = [self processUpdatedProperties:applierBlocks parentProperties:parentProperties]; for (RCTShadowView *child in _reactSubviews) { [child collectUpdatedProperties:applierBlocks parentProperties:nextProps]; } @@ -212,21 +222,19 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor { - CGFloat totalOffsetTop = 0.0; - CGFloat totalOffsetLeft = 0.0; - CGSize size = self.frame.size; + CGPoint offset = CGPointZero; NSInteger depth = 30; // max depth to search RCTShadowView *shadowView = self; while (depth && shadowView && shadowView != ancestor) { - totalOffsetTop += shadowView.frame.origin.y; - totalOffsetLeft += shadowView.frame.origin.x; + offset.x += shadowView.frame.origin.x; + offset.y += shadowView.frame.origin.y; shadowView = shadowView->_superview; depth--; } if (ancestor != shadowView) { return CGRectNull; } - return (CGRect){{totalOffsetLeft, totalOffsetTop}, size}; + return (CGRect){offset, self.frame.size}; } - (instancetype)init diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 2cc03d874..a584a5798 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -277,7 +277,7 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) // View has cliping enabled, so we can easily test if it is partially // or completely within the clipRect, and mount or unmount it accordingly - if (CGRectIntersectsRect(clipRect, view.frame)) { + if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { // View is at least partially visible, so remount it if unmounted if (view.superview == nil) { diff --git a/package.json b/package.json index 535d59a57..19226bc08 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,9 @@ "react-timer-mixin": "^0.13.1", "react-tools": "0.13.2", "rebound": "^0.0.12", - "sane": "git://github.com/tadeuzagallo/sane.git#a029f8b04a", + "sane": "tadeuzagallo/sane#a029f8b04a", "source-map": "0.1.31", - "stacktrace-parser": "git://github.com/frantic/stacktrace-parser.git#493c5e5638", + "stacktrace-parser": "frantic/stacktrace-parser#493c5e5638", "uglify-js": "~2.4.16", "underscore": "1.7.0", "worker-farm": "^1.3.1", diff --git a/packager/packager.js b/packager/packager.js index d630d06e3..7c4be0a77 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -10,7 +10,7 @@ var fs = require('fs'); var path = require('path'); -var exec = require('child_process').exec; +var execFile = require('child_process').execFile; var http = require('http'); var getFlowTypeCheckMiddleware = require('./getFlowTypeCheckMiddleware'); @@ -172,7 +172,7 @@ function getDevToolsLauncher(options) { var debuggerURL = 'http://localhost:' + options.port + '/debugger-ui'; var script = 'launchChromeDevTools.applescript'; console.log('Launching Dev Tools...'); - exec(path.join(__dirname, script) + ' ' + debuggerURL, function(err, stdout, stderr) { + execFile(path.join(__dirname, script), [debuggerURL], function(err, stdout, stderr) { if (err) { console.log('Failed to run ' + script, err); }