diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index d2fc3eade..3928adc33 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; 00E356F31AD99517003FC87E /* SampleAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* SampleAppTests.m */; }; 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -82,6 +83,13 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = SampleApp; }; + 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTSettings; + }; 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; @@ -117,6 +125,7 @@ 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* SampleAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleAppTests.m; sourceTree = ""; }; + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* SampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = iOS/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = iOS/AppDelegate.m; sourceTree = ""; }; @@ -142,6 +151,7 @@ buildActionMask = 2147483647; files = ( 146834051AC3E58100842450 /* libReact.a in Frameworks */, + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, 00481BE81AC0C86700671115 /* libRCTWebSocketDebugger.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, 00C302E61ABCBA2D00DB3ED1 /* libRCTAdSupport.a in Frameworks */, @@ -230,6 +240,14 @@ name = "Supporting Files"; sourceTree = ""; }; + 139105B71AF99BAD00B5F7CC /* Products */ = { + isa = PBXGroup; + children = ( + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; 13B07FAE1A68108700A75B9A /* SampleApp */ = { isa = PBXGroup; children = ( @@ -263,14 +281,15 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( - 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, - 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, 00C302AF1ABCB8E700DB3ED1 /* RCTAdSupport.xcodeproj */, 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */, ); @@ -395,6 +414,10 @@ ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; }, + { + ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; + ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + }, { ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; @@ -470,6 +493,13 @@ remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTSettings.a; + remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 146834041AC3E56700842450 /* libReact.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/SampleApp/index.ios.js b/Examples/SampleApp/index.ios.js index 629118d10..46c0712bd 100644 --- a/Examples/SampleApp/index.ios.js +++ b/Examples/SampleApp/index.ios.js @@ -24,7 +24,7 @@ var SampleApp = React.createClass({ Press Cmd+R to reload,{'\n'} - Cmd+Control+Z for dev menu + Cmd+D or shake for dev menu ); diff --git a/Libraries/Components/View/ViewStylePropTypes.js b/Libraries/Components/View/ViewStylePropTypes.js index a5b591a61..c1f6b4b1c 100644 --- a/Libraries/Components/View/ViewStylePropTypes.js +++ b/Libraries/Components/View/ViewStylePropTypes.js @@ -13,12 +13,14 @@ var LayoutPropTypes = require('LayoutPropTypes'); var ReactPropTypes = require('ReactPropTypes'); +var TransformPropTypes = require('TransformPropTypes'); /** * Warning: Some of these properties may not be supported in all releases. */ var ViewStylePropTypes = { ...LayoutPropTypes, + ...TransformPropTypes, backgroundColor: ReactPropTypes.string, borderColor: ReactPropTypes.string, borderTopColor: ReactPropTypes.string, @@ -34,15 +36,6 @@ var ViewStylePropTypes = { ), shadowOpacity: ReactPropTypes.number, shadowRadius: ReactPropTypes.number, - transform: ReactPropTypes.arrayOf(ReactPropTypes.object), - transformMatrix: ReactPropTypes.arrayOf(ReactPropTypes.number), - - // DEPRECATED - rotation: ReactPropTypes.number, - scaleX: ReactPropTypes.number, - scaleY: ReactPropTypes.number, - translateX: ReactPropTypes.number, - translateY: ReactPropTypes.number, }; module.exports = ViewStylePropTypes; diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index ad9ea0e8a..c009e8086 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -227,15 +227,17 @@ var Navigator = React.createClass({ renderScene: PropTypes.func.isRequired, /** - * Provide a single "route" to start on. A route is an arbitrary object - * that the navigator will use to identify each scene before rendering. - * Either initialRoute or initialRouteStack is required. + * Specify a route to start on. A route is an object that the navigator + * will use to identify each scene to render. `initialRoute` must be + * a route in the `initialRouteStack` if both props are provided. The + * `initialRoute` will default to the last item in the `initialRouteStack`. */ initialRoute: PropTypes.object, /** - * Provide a set of routes to initially mount the scenes for. Required if no - * initialRoute is provided + * Provide a set of routes to initially mount. Required if no initialRoute + * is provided. Otherwise, it will default to an array containing only the + * `initialRoute` */ initialRouteStack: PropTypes.arrayOf(PropTypes.object), @@ -295,21 +297,18 @@ var Navigator = React.createClass({ }, getInitialState: function() { - var routeStack = this.props.initialRouteStack || []; - var initialRouteIndex = 0; - if (this.props.initialRoute && routeStack.length) { + var routeStack = this.props.initialRouteStack || [this.props.initialRoute]; + invariant( + routeStack.length >= 1, + 'Navigator requires props.initialRoute or props.initialRouteStack.' + ); + var initialRouteIndex = routeStack.length - 1; + if (this.props.initialRoute) { initialRouteIndex = routeStack.indexOf(this.props.initialRoute); invariant( initialRouteIndex !== -1, 'initialRoute is not in initialRouteStack.' ); - } else if (this.props.initialRoute) { - routeStack = [this.props.initialRoute]; - } else { - invariant( - routeStack.length >= 1, - 'Navigator requires props.initialRoute or props.initialRouteStack.' - ); } return { sceneConfigStack: routeStack.map( @@ -689,7 +688,8 @@ var Navigator = React.createClass({ var enabledSceneNativeProps = { left: sceneStyle.left, }; - if (sceneIndex !== this.state.transitionFromIndex) { + if (sceneIndex !== this.state.transitionFromIndex && + sceneIndex !== this.state.presentedIndex) { // If we are not in a transition from this index, make sure opacity is 0 // to prevent the enabled scene from flashing over the presented scene enabledSceneNativeProps.opacity = 0; diff --git a/Libraries/Image/ImageStylePropTypes.js b/Libraries/Image/ImageStylePropTypes.js index d46807ce7..c70bee73a 100644 --- a/Libraries/Image/ImageStylePropTypes.js +++ b/Libraries/Image/ImageStylePropTypes.js @@ -14,9 +14,11 @@ var ImageResizeMode = require('ImageResizeMode'); var LayoutPropTypes = require('LayoutPropTypes'); var ReactPropTypes = require('ReactPropTypes'); +var TransformPropTypes = require('TransformPropTypes'); var ImageStylePropTypes = { ...LayoutPropTypes, + ...TransformPropTypes, resizeMode: ReactPropTypes.oneOf(Object.keys(ImageResizeMode)), backgroundColor: ReactPropTypes.string, borderColor: ReactPropTypes.string, diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequestBase.js index 4a4f16ac6..99a327b5f 100644 --- a/Libraries/Network/XMLHttpRequestBase.js +++ b/Libraries/Network/XMLHttpRequestBase.js @@ -66,7 +66,7 @@ class XMLHttpRequestBase { getResponseHeader(header: string): ?string { if (this.responseHeaders) { - var value = this.responseHeaders[header]; + var value = this.responseHeaders[header.toLowerCase()]; return value !== undefined ? value : null; } return null; @@ -132,7 +132,12 @@ class XMLHttpRequestBase { return; } this.status = status; - this.responseHeaders = responseHeaders || {}; + // Headers should be case-insensitive + var lcResponseHeaders = {}; + for (var header in responseHeaders) { + lcResponseHeaders[header.toLowerCase()] = responseHeaders[header]; + } + this.responseHeaders = lcResponseHeaders; this.responseText = responseText; this._setReadyState(this.DONE); this._sendLoad(); diff --git a/Libraries/Settings/RCTSettingsManager.m b/Libraries/Settings/RCTSettingsManager.m index b17439eaf..6f8b146c1 100644 --- a/Libraries/Settings/RCTSettingsManager.m +++ b/Libraries/Settings/RCTSettingsManager.m @@ -12,6 +12,7 @@ #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" +#import "RCTUtils.h" @implementation RCTSettingsManager { @@ -53,13 +54,15 @@ RCT_EXPORT_MODULE() return; } - [_bridge.eventDispatcher sendDeviceEventWithName:@"settingsUpdated" body:[_defaults dictionaryRepresentation]]; + [_bridge.eventDispatcher + sendDeviceEventWithName:@"settingsUpdated" + body:RCTJSONClean([_defaults dictionaryRepresentation])]; } - (NSDictionary *)constantsToExport { return @{ - @"settings": [_defaults dictionaryRepresentation] + @"settings": RCTJSONClean([_defaults dictionaryRepresentation]) }; } diff --git a/Libraries/StyleSheet/TransformPropTypes.js b/Libraries/StyleSheet/TransformPropTypes.js new file mode 100644 index 000000000..9338f7ff5 --- /dev/null +++ b/Libraries/StyleSheet/TransformPropTypes.js @@ -0,0 +1,28 @@ +/** + * 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 TransformPropTypes + * @flow + */ +'use strict'; + +var ReactPropTypes = require('ReactPropTypes'); + +var TransformPropTypes = { + transform: ReactPropTypes.arrayOf(ReactPropTypes.object), + transformMatrix: ReactPropTypes.arrayOf(ReactPropTypes.number), + + // DEPRECATED + rotation: ReactPropTypes.number, + scaleX: ReactPropTypes.number, + scaleY: ReactPropTypes.number, + translateX: ReactPropTypes.number, + translateY: ReactPropTypes.number, +}; + +module.exports = TransformPropTypes; diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 71bf7b302..dd99ac9fb 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -39,6 +39,7 @@ + (NSString *)NSString:(id)json; + (NSNumber *)NSNumber:(id)json; + (NSData *)NSData:(id)json; ++ (NSIndexSet *)NSIndexSet:(id)json; + (NSURL *)NSURL:(id)json; + (NSURLRequest *)NSURLRequest:(id)json; @@ -76,6 +77,7 @@ + (UIImage *)UIImage:(id)json; + (CGImageRef)CGImage:(id)json; ++ (UIFont *)UIFont:(id)json; + (UIFont *)UIFont:(UIFont *)font withSize:(id)json; + (UIFont *)UIFont:(UIFont *)font withWeight:(id)json; + (UIFont *)UIFont:(UIFont *)font withStyle:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 9c27b95d9..06b5aa023 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -64,6 +64,20 @@ RCT_CONVERTER(NSString *, NSString, description) return [[self NSString:json] dataUsingEncoding:NSUTF8StringEncoding]; } ++ (NSIndexSet *)NSIndexSet:(id)json +{ + json = [self NSNumberArray:json]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; + for (NSNumber *number in json) { + NSInteger index = number.integerValue; + if (RCT_DEBUG && index < 0) { + RCTLogError(@"Invalid index value %zd. Indices must be positive.", index); + } + [indexSet addIndex:index]; + } + return indexSet; +} + + (NSURL *)NSURL:(id)json { NSString *path = [self NSString:json]; @@ -679,6 +693,16 @@ static BOOL RCTFontIsCondensed(UIFont *font) return (symbolicTraits & UIFontDescriptorTraitCondensed) != 0; } ++ (UIFont *)UIFont:(id)json +{ + json = [self NSDictionary:json]; + return [self UIFont:nil + withFamily:json[@"fontFamily"] + size:json[@"fontSize"] + weight:json[@"fontWeight"] + style:json[@"fontStyle"]]; +} + + (UIFont *)UIFont:(UIFont *)font withSize:(id)json { return [self UIFont:font withFamily:nil size:json weight:nil style:nil]; @@ -728,11 +752,6 @@ static BOOL RCTFontIsCondensed(UIFont *font) // Get font family familyName = [self NSString:family] ?: familyName; - // Get font style - if (style) { - isItalic = [self RCTFontStyle:style]; - } - // Gracefully handle being given a font name rather than font family, for // example: "Helvetica Light Oblique" rather than just "Helvetica". if ([UIFont fontNamesForFamilyName:familyName].count == 0) { @@ -751,6 +770,11 @@ static BOOL RCTFontIsCondensed(UIFont *font) } } + // Get font style + if (style) { + isItalic = [self RCTFontStyle:style]; + } + // Get font weight if (weight) { fontWeight = [self RCTFontWeight:weight]; @@ -758,13 +782,7 @@ static BOOL RCTFontIsCondensed(UIFont *font) // Get the closest font that matches the given weight for the fontFamily UIFont *bestMatch = [UIFont fontWithName:font.fontName size: fontSize]; - CGFloat closestWeight; - - if (font && [font.familyName isEqualToString: familyName]) { - closestWeight = RCTWeightOfFont(font); - } else { - closestWeight = INFINITY; - } + CGFloat closestWeight = INFINITY; for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) { UIFont *match = [UIFont fontWithName:name size:fontSize]; @@ -834,7 +852,9 @@ static id RCTConvertPropertyListValue(id json) { if (!json || json == (id)kCFNull) { return nil; - } else if ([json isKindOfClass:[NSDictionary class]]) { + } + + if ([json isKindOfClass:[NSDictionary class]]) { __block BOOL copy = NO; NSMutableDictionary *values = [[NSMutableDictionary alloc] initWithCapacity:[json count]]; [json enumerateKeysAndObjectsUsingBlock:^(NSString *key, id jsonValue, BOOL *stop) { @@ -845,7 +865,9 @@ static id RCTConvertPropertyListValue(id json) copy |= value != jsonValue; }]; return copy ? values : json; - } else if ([json isKindOfClass:[NSArray class]]) { + } + + if ([json isKindOfClass:[NSArray class]]) { __block BOOL copy = NO; __block NSArray *values = json; [json enumerateObjectsUsingBlock:^(id jsonValue, NSUInteger idx, BOOL *stop) { @@ -860,15 +882,17 @@ static id RCTConvertPropertyListValue(id json) for (NSInteger i = 0; i < idx; i++) { [(NSMutableArray *)values addObject:json[i]]; } - [(NSMutableArray *)values addObject:value]; + if (value) { + [(NSMutableArray *)values addObject:value]; + } copy = YES; } }]; return values; - } else { - // All other JSON types are supported by property lists - return json; } + + // All other JSON types are supported by property lists + return json; } + (NSPropertyList)NSPropertyList:(id)json diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index d2e4b7416..3d88caf55 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -66,9 +66,7 @@ RCT_EXPORT_MODULE() // We're swizzling here because it's poor form to override methods in a category, // however UIWindow doesn't actually implement motionEnded:withEvent:, so there's // no need to call the original implementation. -#if RCT_DEV RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:)); -#endif } - (instancetype)init @@ -121,8 +119,6 @@ RCT_EXPORT_MODULE() - (void)updateSettings { - _settings = [NSMutableDictionary dictionaryWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]]; - __weak RCTDevMenu *weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ RCTDevMenu *strongSelf = weakSelf; @@ -130,6 +126,8 @@ RCT_EXPORT_MODULE() return; } + strongSelf->_settings = [NSMutableDictionary dictionaryWithDictionary:[strongSelf->_defaults objectForKey:RCTDevMenuSettingsKey]]; + strongSelf.shakeToShow = [strongSelf->_settings[@"shakeToShow"] ?: @YES boolValue]; strongSelf.profilingEnabled = [strongSelf->_settings[@"profilingEnabled"] ?: @NO boolValue]; strongSelf.liveReloadEnabled = [strongSelf->_settings[@"liveReloadEnabled"] ?: @NO boolValue]; diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 1c0412568..6c4b91464 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -19,6 +19,9 @@ RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error); RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error); +// Strip non JSON-safe values from an object graph +RCT_EXTERN id RCTJSONClean(id object); + // Get MD5 hash of a string (TODO: currently unused. Remove?) RCT_EXTERN NSString *RCTMD5Hash(NSString *string); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 3c644dbff..ff11b5764 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -20,7 +20,7 @@ NSString *RCTJSONStringify(id jsonObject, NSError **error) { - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:error]; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:(NSJSONWritingOptions)NSJSONReadingAllowFragments error:error]; return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil; } @@ -42,6 +42,57 @@ id RCTJSONParse(NSString *jsonString, NSError **error) return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error]; } +id RCTJSONClean(id object) +{ + static dispatch_once_t onceToken; + static NSSet *validLeafTypes; + dispatch_once(&onceToken, ^{ + validLeafTypes = [[NSSet alloc] initWithArray:@[ + [NSString class], + [NSMutableString class], + [NSNumber class], + [NSNull class], + ]]; + }); + + if ([validLeafTypes containsObject:[object classForCoder]]) { + return object; + } + + if ([object isKindOfClass:[NSDictionary class]]) { + __block BOOL copy = NO; + NSMutableDictionary *values = [[NSMutableDictionary alloc] initWithCapacity:[object count]]; + [object enumerateKeysAndObjectsUsingBlock:^(NSString *key, id item, BOOL *stop) { + id value = RCTJSONClean(item); + values[key] = value; + copy |= value != item; + }]; + return copy ? values : object; + } + + if ([object isKindOfClass:[NSArray class]]) { + __block BOOL copy = NO; + __block NSArray *values = object; + [object enumerateObjectsUsingBlock:^(id item, NSUInteger idx, BOOL *stop) { + id value = RCTJSONClean(item); + if (copy) { + [(NSMutableArray *)values addObject:value]; + } else if (value != item) { + // Converted value is different, so we'll need to copy the array + values = [[NSMutableArray alloc] initWithCapacity:values.count]; + for (NSInteger i = 0; i < idx; i++) { + [(NSMutableArray *)values addObject:object[i]]; + } + [(NSMutableArray *)values addObject:value]; + copy = YES; + } + }]; + return values; + } + + return (id)kCFNull; +} + NSString *RCTMD5Hash(NSString *string) { const char *str = [string UTF8String]; diff --git a/React/Views/RCTScrollView.h b/React/Views/RCTScrollView.h index f218ea6ea..0333a38a7 100644 --- a/React/Views/RCTScrollView.h +++ b/React/Views/RCTScrollView.h @@ -45,6 +45,6 @@ @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; @property (nonatomic, assign) NSTimeInterval scrollEventThrottle; @property (nonatomic, assign) BOOL centerContent; -@property (nonatomic, copy) NSArray *stickyHeaderIndices; +@property (nonatomic, copy) NSIndexSet *stickyHeaderIndices; @end diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 1b3170064..ca8f94242 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -28,8 +28,8 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; */ @interface RCTCustomScrollView : UIScrollView -@property (nonatomic, copy, readwrite) NSArray *stickyHeaderIndices; -@property (nonatomic, readwrite, assign) BOOL centerContent; +@property (nonatomic, copy) NSIndexSet *stickyHeaderIndices; +@property (nonatomic, assign) BOOL centerContent; @end @@ -155,97 +155,72 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; [super setContentOffset:contentOffset]; } -- (void)setBounds:(CGRect)bounds -{ - [super setBounds:bounds]; - [self dockClosestSectionHeader]; -} - - (void)dockClosestSectionHeader { UIView *contentView = [self contentView]; - if (_stickyHeaderIndices.count == 0 || !contentView) { + CGFloat scrollTop = self.bounds.origin.y + self.contentInset.top; + + // Find the section headers that need to be docked + __block UIView *previousHeader = nil; + __block UIView *currentHeader = nil; + __block UIView *nextHeader = nil; + NSInteger subviewCount = contentView.reactSubviews.count; + [_stickyHeaderIndices enumerateIndexesWithOptions:0 usingBlock:^(NSUInteger idx, BOOL *stop) { + + if (idx >= subviewCount) { + RCTLogError(@"Sticky header index %zd was outside the range {0, %zd}", idx, subviewCount); + return; + } + + UIView *header = contentView.reactSubviews[idx]; + + // If nextHeader not yet found, search for docked headers + if (!nextHeader) { + CGFloat height = header.bounds.size.height; + CGFloat top = header.center.y - height * header.layer.anchorPoint.y; + if (top > scrollTop) { + nextHeader = header; + } else { + previousHeader = currentHeader; + currentHeader = header; + } + } + + // Reset transforms for header views + header.transform = CGAffineTransformIdentity; + header.layer.zPosition = ZINDEX_DEFAULT; + + }]; + + // If no docked header, bail out + if (!currentHeader) { return; } - // find the section header that needs to be docked - NSInteger firstIndexInView = [[_stickyHeaderIndices firstObject] integerValue] + 1; - CGRect scrollBounds = self.bounds; - scrollBounds.origin.x += self.contentInset.left; - scrollBounds.origin.y += self.contentInset.top; - - NSInteger i = 0; - for (UIView *subview in contentView.reactSubviews) { - CGRect rowFrame = [RCTCustomScrollView _calculateUntransformedFrame:subview]; - if (CGRectIntersectsRect(scrollBounds, rowFrame)) { - firstIndexInView = i; - break; - } - i++; + // Adjust current header to hug the top of the screen + CGFloat currentFrameHeight = currentHeader.bounds.size.height; + CGFloat currentFrameTop = currentHeader.center.y - currentFrameHeight * currentHeader.layer.anchorPoint.y; + CGFloat yOffset = scrollTop - currentFrameTop; + if (nextHeader) { + // The next header nudges the current header out of the way when it reaches + // the top of the screen + CGFloat nextFrameHeight = nextHeader.bounds.size.height; + CGFloat nextFrameTop = nextHeader.center.y - nextFrameHeight * nextHeader.layer.anchorPoint.y; + CGFloat overlap = currentFrameHeight - (nextFrameTop - scrollTop); + yOffset -= MAX(0, overlap); } - NSInteger stickyHeaderii = 0; - for (NSNumber *stickyHeaderI in _stickyHeaderIndices) { - if ([stickyHeaderI integerValue] > firstIndexInView) { - break; - } - stickyHeaderii++; - } - stickyHeaderii = MAX(0, stickyHeaderii - 1); - - // Set up transforms for the various section headers - NSInteger currentlyDockedIndex = [_stickyHeaderIndices[stickyHeaderii] integerValue]; - NSInteger previouslyDockedIndex = stickyHeaderii > 0 ? [_stickyHeaderIndices[stickyHeaderii-1] integerValue] : -1; - NSInteger nextDockedIndex = (stickyHeaderii < _stickyHeaderIndices.count - 1) ? - [_stickyHeaderIndices[stickyHeaderii + 1] integerValue] : -1; - - UIView *currentHeader = contentView.reactSubviews[currentlyDockedIndex]; - UIView *previousHeader = previouslyDockedIndex >= 0 ? contentView.reactSubviews[previouslyDockedIndex] : nil; - CGRect curFrame = [RCTCustomScrollView _calculateUntransformedFrame:currentHeader]; + currentHeader.transform = CGAffineTransformMakeTranslation(0, yOffset); + currentHeader.layer.zPosition = ZINDEX_STICKY_HEADER; if (previousHeader) { - // the previous header is offset to sit right above the currentlyDockedHeader's initial position - // (so it scrolls away nicely once the currentHeader locks into position) - CGRect previousFrame = [RCTCustomScrollView _calculateUntransformedFrame:previousHeader]; - CGFloat yOffset = curFrame.origin.y - previousFrame.origin.y - previousFrame.size.height; + // The previous header sits right above the currentHeader's initial position + // so it scrolls away nicely once the currentHeader has locked into place + CGFloat previousFrameHeight = previousHeader.bounds.size.height; + CGFloat targetCenter = currentFrameTop - previousFrameHeight * (1.0 - previousHeader.layer.anchorPoint.y); + yOffset = targetCenter - previousHeader.center.y; previousHeader.transform = CGAffineTransformMakeTranslation(0, yOffset); + previousHeader.layer.zPosition = ZINDEX_STICKY_HEADER; } - - UIView *nextHeader = nextDockedIndex >= 0 ? contentView.reactSubviews[nextDockedIndex] : nil; - CGRect nextFrame = [RCTCustomScrollView _calculateUntransformedFrame:nextHeader]; - - if (curFrame.origin.y < scrollBounds.origin.y) { - // scrolled off (or being scrolled off) the top of the screen - CGFloat yOffset = 0; - if (nextHeader && nextFrame.origin.y < scrollBounds.origin.y + curFrame.size.height) { - // next frame is bumping me off if scrolling down (or i'm bumping the next one off if scrolling up) - yOffset = nextFrame.origin.y - curFrame.origin.y - curFrame.size.height; - } else { - // standard sticky header position - yOffset = scrollBounds.origin.y - curFrame.origin.y; - } - currentHeader.transform = CGAffineTransformMakeTranslation(0, yOffset); - currentHeader.layer.zPosition = ZINDEX_STICKY_HEADER; - } else { - // i'm the current header but in the viewport, so just scroll in normal position - currentHeader.transform = CGAffineTransformIdentity; - currentHeader.layer.zPosition = ZINDEX_DEFAULT; - } - - // in our setup, 'next header' will always just scroll with the page - if (nextHeader) { - nextHeader.transform = CGAffineTransformIdentity; - nextHeader.layer.zPosition = ZINDEX_DEFAULT; - } -} - -+ (CGRect)_calculateUntransformedFrame:(UIView *)view -{ - CGRect frame = CGRectNull; - if (view) { - frame.size = view.bounds.size; - frame.origin = CGPointMake(view.layer.position.x - view.bounds.size.width * view.layer.anchorPoint.x, view.layer.position.y - view.bounds.size.height * view.layer.anchorPoint.y); - } - return frame; } @end @@ -312,7 +287,7 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; _scrollView.centerContent = centerContent; } -- (void)setStickyHeaderIndices:(NSArray *)headerIndices +- (void)setStickyHeaderIndices:(NSIndexSet *)headerIndices { RCTAssert(_scrollView.contentSize.width <= self.frame.size.width, @"sticky headers are not supported with horizontal scrolled views"); @@ -340,14 +315,8 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; - (void)setContentInset:(UIEdgeInsets)contentInset { - CGPoint contentOffset = _scrollView.contentOffset; - _contentInset = contentInset; - [RCTView autoAdjustInsetsForView:self - withScrollView:_scrollView - updateOffset:NO]; - - _scrollView.contentOffset = contentOffset; + [self setNeedsLayout]; } - (void)scrollToOffset:(CGPoint)offset @@ -390,6 +359,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + [_scrollView dockClosestSectionHeader]; [self updateClippedSubviews]; NSTimeInterval now = CACurrentMediaTime(); diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index ededf5f7f..8441de74d 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -41,7 +41,7 @@ RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(scrollsToTop, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL) -RCT_EXPORT_VIEW_PROPERTY(stickyHeaderIndices, NSNumberArray) +RCT_EXPORT_VIEW_PROPERTY(stickyHeaderIndices, NSIndexSet) RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat) RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) diff --git a/package.json b/package.json index 987347a1d..8e059c3fa 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "chalk": "^1.0.0", "connect": "2.8.3", "debug": "~2.1.0", + "graceful-fs": "^3.0.6", "image-size": "0.3.5", "joi": "~5.1.0", "jstransform": "10.1.0", diff --git a/packager/launchChromeDevTools.applescript b/packager/launchChromeDevTools.applescript index 1fe6f4b07..4b718f5bd 100755 --- a/packager/launchChromeDevTools.applescript +++ b/packager/launchChromeDevTools.applescript @@ -10,7 +10,7 @@ on run argv set theURL to item 1 of argv - tell application "Google Chrome" + tell application "Chrome" activate if (count every window) = 0 then diff --git a/packager/react-packager/index.js b/packager/react-packager/index.js index 3a70659cf..a7b6264b7 100644 --- a/packager/react-packager/index.js +++ b/packager/react-packager/index.js @@ -8,6 +8,8 @@ */ 'use strict'; +useGracefulFs(); + var Activity = require('./src/Activity'); var Server = require('./src/Server'); @@ -45,3 +47,16 @@ exports.getDependencies = function(options, main) { return r.dependencies; }); }; + +function useGracefulFs() { + var fs = require('fs'); + var gracefulFs = require('graceful-fs'); + + // A bit sneaky but it's not straightforward to update all the + // modules we depend on. + Object.keys(fs).forEach(function(method) { + if (typeof fs[method] === 'function' && gracefulFs[method]) { + fs[method] = gracefulFs[method]; + } + }); +}