diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index 4aff00e73..cbc3e5b41 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -369,6 +369,25 @@ exports.examples = [ ); }, +}, { + title: 'allowFontScaling attribute', + render: function() { + return ( + + + By default, text will respect Text Size accessibility setting on iOS. + It means that all font sizes will be increased or descreased depending on the value of Text Size setting in + {" "}Settings.app - Display & Brightness - Text Size + + + You can disable scaling for your Text component by passing {"\""}allowFontScaling={"{"}false{"}\""} prop. + + + This text will not scale. + + + ); + }, }]; var styles = StyleSheet.create({ diff --git a/Libraries/ART/RCTConvert+ART.m b/Libraries/ART/RCTConvert+ART.m index 7a607a12c..e6bc09286 100644 --- a/Libraries/ART/RCTConvert+ART.m +++ b/Libraries/ART/RCTConvert+ART.m @@ -87,7 +87,7 @@ RCT_ENUM_CONVERTER(CTTextAlignment, (@{ } NSDictionary *fontDict = dict[@"font"]; - CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"]]; + CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"] scaleMultiplier:1.0]; if (!font) { return frame; } diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 94e0850f2..168cb36fa 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -469,9 +469,9 @@ var ScrollResponderMixin = { this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e); }, - scrollResponderKeyboardDidShow: function() { + scrollResponderKeyboardDidShow: function(e: Event) { this.keyboardWillOpenTo = null; - this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(); + this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e); }, scrollResponderKeyboardDidHide: function() { diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js index 1910c67ca..0be39a8f1 100644 --- a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js @@ -32,7 +32,11 @@ var TabBarIOS = React.createClass({ /** * Background color of the tab bar */ - barTintColor: React.PropTypes.string + barTintColor: React.PropTypes.string, + /** + * A Boolean value that indicates whether the tab bar is translucent + */ + translucent: React.PropTypes.bool, }, render: function() { @@ -40,7 +44,8 @@ var TabBarIOS = React.createClass({ + barTintColor={this.props.barTintColor} + translucent={this.props.translucent !== false}> {this.props.children} ); diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 0cb6e4a4f..6912c05db 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -44,6 +44,12 @@ var AccessibilityTraits = [ 'pageTurn', ]; +// <<<<< WARNING >>>>> +// If adding any properties to View that could change the way layout-only status +// works on iOS, make sure to update ReactNativeViewAttributes.js and +// RCTShadowView.m (in the -[RCTShadowView isLayoutOnly] method). +// <<<<< WARNING >>>>> + /** * The most fundamental component for building UI, `View` is a * container that supports layout with flexbox, style, some touch handling, and diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 496ff4bb2..16faeb100 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -29,7 +29,7 @@ var ListViewDataSource = require('ListViewDataSource'); var React = require('React'); var RCTUIManager = require('NativeModules').UIManager; -var RKScrollViewManager = require('NativeModules').ScrollViewManager; +var RCTScrollViewManager = require('NativeModules').ScrollViewManager; var ScrollView = require('ScrollView'); var ScrollResponder = require('ScrollResponder'); var StaticRenderer = require('StaticRenderer'); @@ -417,9 +417,10 @@ var ListView = React.createClass({ this._setScrollVisibleHeight ); - // RKScrollViewManager.calculateChildFrames not available on every platform - RKScrollViewManager && RKScrollViewManager.calculateChildFrames && - RKScrollViewManager.calculateChildFrames( + // RCTScrollViewManager.calculateChildFrames is not available on + // every platform + RCTScrollViewManager && RCTScrollViewManager.calculateChildFrames && + RCTScrollViewManager.calculateChildFrames( React.findNodeHandle(scrollComponent), this._updateChildFrames, ); diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 0127cfd8e..8ecabbafd 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; }; 1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; }; 1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1345A8381B26592900583190 /* RCTImageRequestHandler.m */; }; + 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */; }; 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137620341B31C53500677FF0 /* RCTImagePickerManager.m */; }; 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; @@ -43,6 +44,8 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = ""; }; 1345A8371B26592900583190 /* RCTImageRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageRequestHandler.h; sourceTree = ""; }; 1345A8381B26592900583190 /* RCTImageRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageRequestHandler.m; sourceTree = ""; }; + 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageUtils.h; sourceTree = ""; }; + 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtils.m; sourceTree = ""; }; 137620331B31C53500677FF0 /* RCTImagePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImagePickerManager.h; sourceTree = ""; }; 137620341B31C53500677FF0 /* RCTImagePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImagePickerManager.m; sourceTree = ""; }; 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCameraRollManager.h; sourceTree = ""; }; @@ -94,6 +97,8 @@ 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */, 1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */, 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */, + 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */, + 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */, 58B5115E1A9E6B3D00147676 /* Products */, ); indentWidth = 2; @@ -175,6 +180,7 @@ 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */, 03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */, + 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index 0f9cad198..697214249 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -10,6 +10,7 @@ #import "RCTImageDownloader.h" #import "RCTDownloadTaskWrapper.h" +#import "RCTImageUtils.h" #import "RCTLog.h" #import "RCTUtils.h" @@ -195,82 +196,3 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode); } @end - -/** - * Returns the optimal context size for an image drawn using the clip rect - * returned by RCTClipRect. - */ -CGSize RCTTargetSizeForClipRect(CGRect clipRect) -{ - return (CGSize){ - clipRect.size.width + clipRect.origin.x * 2, - clipRect.size.height + clipRect.origin.y * 2 - }; -} - -/** - * This function takes an input content size & scale (typically from an image), - * a target size & scale that it will be drawn into (typically a CGContext) and - * then calculates the optimal rectangle to draw the image into so that it will - * be sized and positioned correctly if drawn using the specified content mode. - */ -CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, - CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode) -{ - // Precompensate for scale - CGFloat scale = sourceScale / destScale; - sourceSize.width *= scale; - sourceSize.height *= scale; - - // Calculate aspect ratios if needed (don't bother is resizeMode == stretch) - CGFloat aspect = 0.0, targetAspect = 0.0; - if (resizeMode != UIViewContentModeScaleToFill) { - aspect = sourceSize.width / sourceSize.height; - targetAspect = destSize.width / destSize.height; - if (aspect == targetAspect) { - resizeMode = UIViewContentModeScaleToFill; - } - } - - switch (resizeMode) { - case UIViewContentModeScaleToFill: // stretch - - sourceSize.width = MIN(destSize.width, sourceSize.width); - sourceSize.height = MIN(destSize.height, sourceSize.height); - return (CGRect){CGPointZero, sourceSize}; - - case UIViewContentModeScaleAspectFit: // contain - - if (targetAspect <= aspect) { // target is taller than content - sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); - sourceSize.height = sourceSize.width / aspect; - } else { // target is wider than content - sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); - sourceSize.width = sourceSize.height * aspect; - } - return (CGRect){CGPointZero, sourceSize}; - - case UIViewContentModeScaleAspectFill: // cover - - if (targetAspect <= aspect) { // target is taller than content - - sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); - sourceSize.width = sourceSize.height * aspect; - destSize.width = destSize.height * targetAspect; - return (CGRect){{(destSize.width - sourceSize.width) / 2, 0}, sourceSize}; - - } else { // target is wider than content - - sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); - sourceSize.height = sourceSize.width / aspect; - destSize.height = destSize.width / targetAspect; - return (CGRect){{0, (destSize.height - sourceSize.height) / 2}, sourceSize}; - } - - default: - - RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode); - return (CGRect){CGPointZero, destSize}; - } -} diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index 186a53cd1..4337836fd 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -7,20 +7,37 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import +#import @class ALAssetsLibrary; -@class UIImage; @interface RCTImageLoader : NSObject +/** + * The shared asset library instance. + */ + (ALAssetsLibrary *)assetsLibrary; /** * Can be called from any thread. * Will always call callback on main thread. */ -+ (void)loadImageWithTag:(NSString *)tag ++ (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback; +/** + * As above, but includes target size, scale and resizeMode, which are used to + * select the optimal dimensions for the loaded image. + */ ++ (void)loadImageWithTag:(NSString *)imageTag + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback; + +/** + * Is the specified image tag an asset library image? + */ ++ (BOOL)isAssetLibraryImage:(NSString *)imageTag; + @end diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 0e4a9c171..69d98a60a 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -19,6 +19,7 @@ #import "RCTDefines.h" #import "RCTGIFImage.h" #import "RCTImageDownloader.h" +#import "RCTImageUtils.h" #import "RCTLog.h" #import "RCTUtils.h" @@ -56,13 +57,23 @@ static dispatch_queue_t RCTImageLoaderQueue(void) return assetsLibrary; } -/** - * Can be called from any thread. - * Will always call callback on main thread. - */ -+ (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error, id image))callback ++ (void)loadImageWithTag:(NSString *)imageTag + callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback { - if ([imageTag hasPrefix:@"assets-library"]) { + return [self loadImageWithTag:imageTag + size:CGSizeZero + scale:0 + resizeMode:UIViewContentModeScaleToFill + callback:callback]; +} + ++ (void)loadImageWithTag:(NSString *)imageTag + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + callback:(void (^)(NSError *error, id image))callback +{ + if ([imageTag hasPrefix:@"assets-library://"]) { [[RCTImageLoader assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) { if (asset) { // ALAssetLibrary API is async and will be multi-threaded. Loading a few full @@ -73,9 +84,31 @@ static dispatch_queue_t RCTImageLoaderQueue(void) // Also make sure the image is released immediately after it's used so it // doesn't spike the memory up during the process. @autoreleasepool { - ALAssetRepresentation *representation = [asset defaultRepresentation]; - ALAssetOrientation orientation = [representation orientation]; - UIImage *image = [UIImage imageWithCGImage:[representation fullResolutionImage] scale:1.0f orientation:(UIImageOrientation)orientation]; + + BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); + ALAssetOrientation orientation = ALAssetOrientationUp; + CGImageRef imageRef = NULL; + + if (!useMaximumSize) { + imageRef = asset.thumbnail; + } + if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) { + if (!useMaximumSize) { + imageRef = asset.aspectRatioThumbnail; + } + if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) { + ALAssetRepresentation *representation = [asset defaultRepresentation]; + orientation = [representation orientation]; + if (!useMaximumSize) { + imageRef = [representation fullScreenImage]; + } + if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) { + imageRef = [representation fullResolutionImage]; + } + } + } + + UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale orientation:(UIImageOrientation)orientation]; RCTDispatchCallbackOnMainQueue(callback, nil, image); } }); @@ -91,9 +124,9 @@ static dispatch_queue_t RCTImageLoaderQueue(void) }]; } else if ([imageTag hasPrefix:@"ph://"]) { // Using PhotoKit for iOS 8+ - // 'ph://' prefix is used by FBMediaKit to differentiate between assets-library. It is prepended to the local ID so that it - // is in the form of NSURL which is what assets-library is based on. - // This means if we use any FB standard photo picker, we will get this prefix =( + // The 'ph://' prefix is used by FBMediaKit to differentiate between + // assets-library. It is prepended to the local ID so that it is in the + // form of an, NSURL which is what assets-library uses. NSString *phAssetID = [imageTag substringFromIndex:[@"ph://" length]]; PHFetchResult *results = [PHAsset fetchAssetsWithLocalIdentifiers:@[phAssetID] options:nil]; if (results.count == 0) { @@ -104,7 +137,12 @@ static dispatch_queue_t RCTImageLoaderQueue(void) } PHAsset *asset = [results firstObject]; - [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:nil resultHandler:^(UIImage *result, NSDictionary *info) { + CGSize targetSize = CGSizeEqualToSize(size, CGSizeZero) ? PHImageManagerMaximumSize : size; + PHImageContentMode contentMode = PHImageContentModeAspectFill; + if (resizeMode == UIViewContentModeScaleAspectFit) { + contentMode = PHImageContentModeAspectFit; + } + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:nil resultHandler:^(UIImage *result, NSDictionary *info) { if (result) { RCTDispatchCallbackOnMainQueue(callback, nil, result); } else { @@ -121,13 +159,20 @@ static dispatch_queue_t RCTImageLoaderQueue(void) RCTDispatchCallbackOnMainQueue(callback, RCTErrorWithMessage(errorMessage), nil); return; } - [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:nil block:^(NSData *data, NSError *error) { - if (error) { - RCTDispatchCallbackOnMainQueue(callback, error, nil); - } else { - RCTDispatchCallbackOnMainQueue(callback, nil, [UIImage imageWithData:data]); - } - }]; + if ([[imageTag lowercaseString] hasSuffix:@".gif"]) { + [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:nil block:^(NSData *data, NSError *error) { + id image = RCTGIFImageWithFileURL([RCTConvert NSURL:imageTag]); + if (!image && !error) { + NSString *errorMessage = [NSString stringWithFormat:@"Unable to load GIF image: %@", imageTag]; + error = RCTErrorWithMessage(errorMessage); + } + RCTDispatchCallbackOnMainQueue(callback, error, image); + }]; + } else { + [[RCTImageDownloader sharedInstance] downloadImageForURL:url size:size scale:scale resizeMode:resizeMode tintColor:nil backgroundColor:nil progressBlock:NULL block:^(UIImage *image, NSError *error) { + RCTDispatchCallbackOnMainQueue(callback, error, image); + }]; + } } else if ([[imageTag lowercaseString] hasSuffix:@".gif"]) { id image = RCTGIFImageWithFileURL([RCTConvert NSURL:imageTag]); if (image) { @@ -149,4 +194,9 @@ static dispatch_queue_t RCTImageLoaderQueue(void) } } ++ (BOOL)isAssetLibraryImage:(NSString *)imageTag +{ + return [imageTag hasPrefix:@"assets-library://"] || [imageTag hasPrefix:@"ph:"]; +} + @end diff --git a/Libraries/Image/RCTImageUtils.h b/Libraries/Image/RCTImageUtils.h new file mode 100644 index 000000000..cbb38cda8 --- /dev/null +++ b/Libraries/Image/RCTImageUtils.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2013, 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. + * + */ + +#import + +#import "RCTDefines.h" + +/** + * Returns the optimal context size for an image drawn using the clip rect + * returned by RCTClipRect. + */ +RCT_EXTERN CGSize RCTTargetSizeForClipRect(CGRect clipRect); + +/** + * This function takes an input content size & scale (typically from an image), + * a target size & scale that it will be drawn into (typically a CGContext) and + * then calculates the optimal rectangle to draw the image into so that it will + * be sized and positioned correctly if drawn using the specified content mode. + */ +RCT_EXTERN CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode); + +/** + * This function takes an input content size & scale (typically from an image), + * a target size & scale that it will be displayed at, and determines if the + * source will need to be upscaled to fit (which may result in pixelization). + */ +RCT_EXTERN BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode); diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m new file mode 100644 index 000000000..89d269532 --- /dev/null +++ b/Libraries/Image/RCTImageUtils.m @@ -0,0 +1,147 @@ +/** + * 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. + */ + +#import "RCTImageUtils.h" + +#import "RCTLog.h" + +CGSize RCTTargetSizeForClipRect(CGRect clipRect) +{ + return (CGSize){ + clipRect.size.width + clipRect.origin.x * 2, + clipRect.size.height + clipRect.origin.y * 2 + }; +} + +CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode) +{ + if (CGSizeEqualToSize(destSize, CGSizeZero)) { + // Assume we require the largest size available + return (CGRect){CGPointZero, sourceSize}; + } + + // Precompensate for scale + CGFloat scale = sourceScale / destScale; + sourceSize.width *= scale; + sourceSize.height *= scale; + + // Calculate aspect ratios if needed (don't bother if resizeMode == stretch) + CGFloat aspect = 0.0, targetAspect = 0.0; + if (resizeMode != UIViewContentModeScaleToFill) { + aspect = sourceSize.width / sourceSize.height; + targetAspect = destSize.width / destSize.height; + if (aspect == targetAspect) { + resizeMode = UIViewContentModeScaleToFill; + } + } + + switch (resizeMode) { + case UIViewContentModeScaleToFill: // stretch + + sourceSize.width = MIN(destSize.width, sourceSize.width); + sourceSize.height = MIN(destSize.height, sourceSize.height); + return (CGRect){CGPointZero, sourceSize}; + + case UIViewContentModeScaleAspectFit: // contain + + if (targetAspect <= aspect) { // target is taller than content + + sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); + sourceSize.height = sourceSize.width / aspect; + + } else { // target is wider than content + + sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); + sourceSize.width = sourceSize.height * aspect; + } + return (CGRect){CGPointZero, sourceSize}; + + case UIViewContentModeScaleAspectFill: // cover + + if (targetAspect <= aspect) { // target is taller than content + + sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); + sourceSize.width = sourceSize.height * aspect; + destSize.width = destSize.height * targetAspect; + return (CGRect){{(destSize.width - sourceSize.width) / 2, 0}, sourceSize}; + + } else { // target is wider than content + + sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); + sourceSize.height = sourceSize.width / aspect; + destSize.height = destSize.width / targetAspect; + return (CGRect){{0, (destSize.height - sourceSize.height) / 2}, sourceSize}; + } + + default: + + RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode); + return (CGRect){CGPointZero, destSize}; + } +} + +RCT_EXTERN BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode) +{ + if (CGSizeEqualToSize(destSize, CGSizeZero)) { + // Assume we require the largest size available + return YES; + } + + // Precompensate for scale + CGFloat scale = sourceScale / destScale; + sourceSize.width *= scale; + sourceSize.height *= scale; + + // Calculate aspect ratios if needed (don't bother if resizeMode == stretch) + CGFloat aspect = 0.0, targetAspect = 0.0; + if (resizeMode != UIViewContentModeScaleToFill) { + aspect = sourceSize.width / sourceSize.height; + targetAspect = destSize.width / destSize.height; + if (aspect == targetAspect) { + resizeMode = UIViewContentModeScaleToFill; + } + } + + switch (resizeMode) { + case UIViewContentModeScaleToFill: // stretch + + return destSize.width > sourceSize.width || destSize.height > sourceSize.height; + + case UIViewContentModeScaleAspectFit: // contain + + if (targetAspect <= aspect) { // target is taller than content + + return destSize.width > sourceSize.width; + + } else { // target is wider than content + + return destSize.height > sourceSize.height; + } + + case UIViewContentModeScaleAspectFill: // cover + + if (targetAspect <= aspect) { // target is taller than content + + return destSize.height > sourceSize.height; + + } else { // target is wider than content + + return destSize.width > sourceSize.width; + } + + default: + + RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode); + return NO; + } +} diff --git a/Libraries/Image/RCTStaticImage.h b/Libraries/Image/RCTStaticImage.h index eb82b597c..c8f46a302 100644 --- a/Libraries/Image/RCTStaticImage.h +++ b/Libraries/Image/RCTStaticImage.h @@ -13,5 +13,6 @@ @property (nonatomic, assign) UIEdgeInsets capInsets; @property (nonatomic, assign) UIImageRenderingMode renderingMode; +@property (nonatomic, copy) NSString *src; @end diff --git a/Libraries/Image/RCTStaticImage.m b/Libraries/Image/RCTStaticImage.m index f9bef7c5d..0e9d4b608 100644 --- a/Libraries/Image/RCTStaticImage.m +++ b/Libraries/Image/RCTStaticImage.m @@ -9,6 +9,13 @@ #import "RCTStaticImage.h" +#import "RCTConvert.h" +#import "RCTGIFImage.h" +#import "RCTImageLoader.h" +#import "RCTUtils.h" + +#import "UIView+React.h" + @implementation RCTStaticImage - (void)_updateImage @@ -59,4 +66,54 @@ } } +- (void)setSrc:(NSString *)src +{ + if (![src isEqual:_src]) { + _src = [src copy]; + [self reloadImage]; + } +} + +- (void)reloadImage +{ + if (_src && !CGSizeEqualToSize(self.frame.size, CGSizeZero)) { + [RCTImageLoader loadImageWithTag:_src + size:self.bounds.size + scale:RCTScreenScale() + resizeMode:self.contentMode callback:^(NSError *error, id image) { + if (error) { + RCTLogWarn(@"%@", error.localizedDescription); + } + if ([image isKindOfClass:[CAAnimation class]]) { + [self.layer addAnimation:image forKey:@"contents"]; + } else { + [self.layer removeAnimationForKey:@"contents"]; + self.image = image; + } + }]; + } else { + [self.layer removeAnimationForKey:@"contents"]; + self.image = nil; + } +} + +- (void)reactSetFrame:(CGRect)frame +{ + [super reactSetFrame:frame]; + if (self.image == nil) { + [self reloadImage]; + } else if ([RCTImageLoader isAssetLibraryImage:_src]) { + CGSize imageSize = { + self.image.size.width / RCTScreenScale(), + self.image.size.height / RCTScreenScale() + }; + CGFloat widthChangeFraction = imageSize.width ? ABS(imageSize.width - frame.size.width) / imageSize.width : 1; + CGFloat heightChangeFraction = imageSize.height ? ABS(imageSize.height - frame.size.height) / imageSize.height : 1; + // If the combined change is more than 20%, reload the asset in case there is a better size. + if (widthChangeFraction + heightChangeFraction > 0.2) { + [self reloadImage]; + } + } +} + @end diff --git a/Libraries/Image/RCTStaticImageManager.m b/Libraries/Image/RCTStaticImageManager.m index bdc6f0596..7b3fb16db 100644 --- a/Libraries/Image/RCTStaticImageManager.m +++ b/Libraries/Image/RCTStaticImageManager.m @@ -12,8 +12,6 @@ #import #import "RCTConvert.h" -#import "RCTGIFImage.h" -#import "RCTImageLoader.h" #import "RCTStaticImage.h" @implementation RCTStaticImageManager @@ -26,21 +24,9 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets) +RCT_REMAP_VIEW_PROPERTY(imageTag, src, NSString) RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) -RCT_CUSTOM_VIEW_PROPERTY(src, NSURL, RCTStaticImage) -{ - if (json) { - if ([[[json description] pathExtension] caseInsensitiveCompare:@"gif"] == NSOrderedSame) { - [view.layer addAnimation:RCTGIFImageWithFileURL([RCTConvert NSURL:json]) forKey:@"contents"]; - } else { - [view.layer removeAnimationForKey:@"contents"]; - view.image = [RCTConvert UIImage:json]; - } - } else { - [view.layer removeAnimationForKey:@"contents"]; - view.image = defaultView.image; - } -} +RCT_EXPORT_VIEW_PROPERTY(src, NSString) RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTStaticImage) { if (json) { @@ -51,24 +37,5 @@ RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTStaticImage) view.tintColor = defaultView.tintColor; } } -RCT_CUSTOM_VIEW_PROPERTY(imageTag, NSString, RCTStaticImage) -{ - if (json) { - [RCTImageLoader loadImageWithTag:[RCTConvert NSString:json] callback:^(NSError *error, id image) { - if (error) { - RCTLogWarn(@"%@", error.localizedDescription); - } - if ([image isKindOfClass:[CAAnimation class]]) { - [view.layer addAnimation:image forKey:@"contents"]; - } else { - [view.layer removeAnimationForKey:@"contents"]; - view.image = image; - } - }]; - } else { - [view.layer removeAnimationForKey:@"contents"]; - view.image = defaultView.image; - } -} @end diff --git a/Libraries/PushNotificationIOS/PushNotificationIOS.js b/Libraries/PushNotificationIOS/PushNotificationIOS.js index 1781ddff1..c71722ae8 100644 --- a/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -36,6 +36,31 @@ class PushNotificationIOS { _sound: string; _badgeCount: number; + /** + * Schedules the localNotification for immediate presentation. + * + * details is an object containing: + * + * - `alertBody` : The message displayed in the notification alert. + * + */ + static presentLocalNotification(details: Object) { + RCTPushNotificationManager.presentLocalNotification(details); + } + + /** + * Schedules the localNotification for future presentation. + * + * details is an object containing: + * + * - `fireDate` : The date and time when the system should deliver the notification. + * - `alertBody` : The message displayed in the notification alert. + * + */ + static scheduleLocalNotification(details: Object) { + RCTPushNotificationManager.scheduleLocalNotification(details); + } + /** * Sets the badge number for the app icon on the home screen */ diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 6a9420819..ac683fc2a 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -10,6 +10,7 @@ #import "RCTPushNotificationManager.h" #import "RCTBridge.h" +#import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTUtils.h" @@ -26,6 +27,19 @@ NSString *const RCTRemoteNotificationReceived = @"RemoteNotificationReceived"; NSString *const RCTRemoteNotificationsRegistered = @"RemoteNotificationsRegistered"; +@implementation RCTConvert (UILocalNotification) + ++ (UILocalNotification *)UILocalNotification:(id)json +{ + NSDictionary *details = [self NSDictionary:json]; + UILocalNotification *notification = [[UILocalNotification alloc] init]; + notification.fireDate = [RCTConvert NSDate:details[@"fireDate"]] ?: [NSDate date]; + notification.alertBody = [RCTConvert NSString:details[@"alertBody"]]; + return notification; +} + +@end + @implementation RCTPushNotificationManager { NSDictionary *_initialNotification; @@ -139,11 +153,15 @@ RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions) } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 + id notificationSettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; [[UIApplication sharedApplication] registerForRemoteNotifications]; + #else + [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types]; + #endif } @@ -183,4 +201,15 @@ RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) }; } +RCT_EXPORT_METHOD(presentLocalNotification:(UILocalNotification *)notification) +{ + [[UIApplication sharedApplication] presentLocalNotificationNow:notification]; +} + + +RCT_EXPORT_METHOD(scheduleLocalNotification:(UILocalNotification *)notification) +{ + [[UIApplication sharedApplication] scheduleLocalNotification:notification]; +} + @end diff --git a/Libraries/ReactNative/ReactNativeViewAttributes.js b/Libraries/ReactNative/ReactNativeViewAttributes.js index eb3a396ca..367c6be2d 100644 --- a/Libraries/ReactNative/ReactNativeViewAttributes.js +++ b/Libraries/ReactNative/ReactNativeViewAttributes.js @@ -26,7 +26,11 @@ ReactNativeViewAttributes.UIView = { onMagicTap: true, collapsible: true, - // If any below are set, view should not be collapsible! + // If editing layout-only view attributes, make sure + // -[RCTShadowView isLayoutOnly] in RCTShadowView.m + // is up-to-date! If any property below is set, the + // view should not be collapsible, but this is done + // on the native side. onMoveShouldSetResponder: true, onResponderGrant: true, onResponderMove: true, diff --git a/Libraries/Text/RCTShadowRawText.m b/Libraries/Text/RCTShadowRawText.m index 00a3490bc..4f79d64b5 100644 --- a/Libraries/Text/RCTShadowRawText.m +++ b/Libraries/Text/RCTShadowRawText.m @@ -9,8 +9,32 @@ #import "RCTShadowRawText.h" +#import "RCTUIManager.h" + @implementation RCTShadowRawText +- (instancetype)init +{ + if ((self = [super init])) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(contentSizeMultiplierDidChange:) + name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)contentSizeMultiplierDidChange:(NSNotification *)note +{ + [self dirtyLayout]; + [self dirtyText]; +} + - (void)setText:(NSString *)text { if (_text != text) { diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h index fe87e99c1..abb111879 100644 --- a/Libraries/Text/RCTShadowText.h +++ b/Libraries/Text/RCTShadowText.h @@ -30,6 +30,8 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, strong) UIColor *textDecorationColor; @property (nonatomic, assign) NSUnderlineStyle textDecorationStyle; @property (nonatomic, assign) RCTTextDecorationLineType textDecorationLine; +@property (nonatomic, assign) CGFloat fontSizeMultiplier; +@property (nonatomic, assign) BOOL allowFontScaling; - (void)recomputeText; diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 621df1ed6..adcca5aed 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -9,6 +9,9 @@ #import "RCTShadowText.h" +#import "RCTAccessibilityManager.h" +#import "RCTUIManager.h" +#import "RCTBridge.h" #import "RCTConvert.h" #import "RCTLog.h" #import "RCTShadowRawText.h" @@ -51,16 +54,31 @@ static css_dim_t RCTMeasure(void *context, float width) _letterSpacing = NAN; _isHighlighted = NO; _textDecorationStyle = NSUnderlineStyleSingle; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(contentSizeMultiplierDidChange:) + name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification + object:nil]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (NSString *)description { NSString *superDescription = super.description; return [[superDescription substringToIndex:superDescription.length - 1] stringByAppendingFormat:@"; text: %@>", [self attributedString].string]; } +- (void)contentSizeMultiplierDidChange:(NSNotification *)note +{ + [self dirtyLayout]; + [self dirtyText]; +} + - (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties { @@ -190,7 +208,9 @@ static css_dim_t RCTMeasure(void *context, float width) [self _addAttribute:NSBackgroundColorAttributeName withValue:self.backgroundColor toAttributedString:attributedString]; } - UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily size:fontSize weight:fontWeight style:fontStyle]; + UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily + size:fontSize weight:fontWeight style:fontStyle + scaleMultiplier:(_allowFontScaling && _fontSizeMultiplier > 0.0 ? _fontSizeMultiplier : 1.0)]; [self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString]; [self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString]; [self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString]; @@ -247,8 +267,9 @@ static css_dim_t RCTMeasure(void *context, float width) NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; paragraphStyle.alignment = _textAlign; paragraphStyle.baseWritingDirection = _writingDirection; - paragraphStyle.minimumLineHeight = _lineHeight; - paragraphStyle.maximumLineHeight = _lineHeight; + CGFloat lineHeight = round(_lineHeight * self.fontSizeMultiplier); + paragraphStyle.minimumLineHeight = lineHeight; + paragraphStyle.maximumLineHeight = lineHeight; [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:(NSRange){0, attributedString.length}]; @@ -321,4 +342,26 @@ RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLine RCT_TEXT_PROPERTY(TextDecorationStyle, _textDecorationStyle, NSUnderlineStyle); RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection) +- (void)setAllowFontScaling:(BOOL)allowFontScaling +{ + _allowFontScaling = allowFontScaling; + for (RCTShadowView *child in [self reactSubviews]) { + if ([child isKindOfClass:[RCTShadowText class]]) { + [(RCTShadowText *)child setAllowFontScaling:allowFontScaling]; + } + } + [self dirtyText]; +} + +- (void)setFontSizeMultiplier:(CGFloat)fontSizeMultiplier +{ + _fontSizeMultiplier = fontSizeMultiplier; + for (RCTShadowView *child in [self reactSubviews]) { + if ([child isKindOfClass:[RCTShadowText class]]) { + [(RCTShadowText *)child setFontSizeMultiplier:fontSizeMultiplier]; + } + } + [self dirtyText]; +} + @end diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index b5f959b73..06d52088a 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -9,6 +9,7 @@ #import "RCTTextManager.h" +#import "RCTAccessibilityManager.h" #import "RCTAssert.h" #import "RCTConvert.h" #import "RCTLog.h" @@ -49,6 +50,7 @@ RCT_EXPORT_SHADOW_PROPERTY(textDecorationStyle, NSUnderlineStyle) RCT_EXPORT_SHADOW_PROPERTY(textDecorationColor, UIColor) RCT_EXPORT_SHADOW_PROPERTY(textDecorationLine, RCTTextDecorationLineType) RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection) +RCT_EXPORT_SHADOW_PROPERTY(allowFontScaling, BOOL) - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry { @@ -69,6 +71,7 @@ RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection) RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text"); if ([shadowView isKindOfClass:[RCTShadowText class]]) { + [(RCTShadowText *)shadowView setFontSizeMultiplier:self.bridge.accessibilityManager.multiplier]; [(RCTShadowText *)shadowView recomputeText]; } else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) { RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index d02733749..7b370ac39 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -30,6 +30,7 @@ var viewConfig = { validAttributes: merge(ReactNativeViewAttributes.UIView, { isHighlighted: true, numberOfLines: true, + allowFontScaling: true, }), uiViewClassName: 'RCTText', }; @@ -99,16 +100,27 @@ var Text = React.createClass({ * * {nativeEvent: {layout: {x, y, width, height}}}. */ - onLayout: React.PropTypes.func, + onLayout: React.PropTypes.func, + /** + * Specifies should fonts scale to respect Text Size accessibility setting. + */ + allowFontScaling: React.PropTypes.bool, }, viewConfig: viewConfig, - getInitialState: function() { + getInitialState: function(): Object { return merge(this.touchableGetInitialState(), { isHighlighted: false, }); }, + + getDefaultProps: function(): Object { + return { + numberOfLines: 0, + allowFontScaling: true, + }; + }, onStartShouldSetResponder: function(): bool { var shouldSetFromProps = this.props.onStartShouldSetResponder && @@ -231,6 +243,7 @@ if (Platform.OS === 'android') { RCTVirtualText = createReactNativeComponentClass({ validAttributes: merge(ReactNativeViewAttributes.UIView, { isHighlighted: true, + allowFontScaling: false, }), uiViewClassName: 'RCTVirtualText', }); diff --git a/React/Base/RCTCache.h b/React/Base/RCTCache.h index 9a4bef4df..67e26c3b7 100644 --- a/React/Base/RCTCache.h +++ b/React/Base/RCTCache.h @@ -15,7 +15,7 @@ * outside of the specified cost/count limits, and will be automatically * cleared in the event of a memory warning. */ -@interface RCTCache : NSCache +@interface RCTCache : NSCache /** * The total number of objects currently resident in the cache. @@ -33,6 +33,11 @@ - (id)objectForKeyedSubscript:(id)key; - (void)setObject:(id)obj forKeyedSubscript:(id)key; +/** + * Enumerate cached objects + */ +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block; + @end @protocol RCTCacheDelegate diff --git a/React/Base/RCTCache.m b/React/Base/RCTCache.m index 073e3faaa..a0646a401 100644 --- a/React/Base/RCTCache.m +++ b/React/Base/RCTCache.m @@ -28,15 +28,6 @@ @implementation RCTCacheEntry -+ (instancetype)entryWithObject:(id)object cost:(NSUInteger)cost sequenceNumber:(NSInteger)sequenceNumber -{ - RCTCacheEntry *entry = [[self alloc] init]; - entry.object = object; - entry.cost = cost; - entry.sequenceNumber = sequenceNumber; - return entry; -} - @end @interface RCTCache_Private : NSObject @@ -46,24 +37,28 @@ @property (nonatomic, assign) NSUInteger totalCostLimit; @property (nonatomic, copy) NSString *name; -@property (nonatomic, assign) NSUInteger totalCost; @property (nonatomic, strong) NSMutableDictionary *cache; -@property (nonatomic, assign) BOOL delegateRespondsToWillEvictObject; -@property (nonatomic, assign) BOOL delegateRespondsToShouldEvictObject; -@property (nonatomic, assign) BOOL currentlyCleaning; +@property (nonatomic, assign) NSUInteger totalCost; @property (nonatomic, assign) NSInteger sequenceNumber; -@property (nonatomic, strong) NSLock *lock; @end @implementation RCTCache_Private +{ + BOOL _delegateRespondsToWillEvictObject; + BOOL _delegateRespondsToShouldEvictObject; + BOOL _currentlyCleaning; + NSMutableArray *_entryPool; + NSLock *_lock; +} -- (instancetype)init +- (id)init { if ((self = [super init])) { //create storage _cache = [[NSMutableDictionary alloc] init]; + _entryPool = [[NSMutableArray alloc] init]; _lock = [[NSLock alloc] init]; _totalCost = 0; @@ -95,7 +90,7 @@ [_lock lock]; _countLimit = countLimit; [_lock unlock]; - [self cleanUp]; + [self cleanUp:NO]; } - (void)setTotalCostLimit:(NSUInteger)totalCostLimit @@ -103,7 +98,7 @@ [_lock lock]; _totalCostLimit = totalCostLimit; [_lock unlock]; - [self cleanUp]; + [self cleanUp:NO]; } - (NSUInteger)count @@ -111,40 +106,51 @@ return [_cache count]; } -- (void)cleanUp +- (void)cleanUp:(BOOL)keepEntries { [_lock lock]; - NSUInteger maxCount = [self countLimit] ?: INT_MAX; - NSUInteger maxCost = [self totalCostLimit] ?: INT_MAX; - NSUInteger totalCount = [_cache count]; - if (totalCount > maxCount || _totalCost > maxCost) + NSUInteger maxCount = _countLimit ?: INT_MAX; + NSUInteger maxCost = _totalCostLimit ?: INT_MAX; + NSUInteger totalCount = _cache.count; + NSMutableArray *keys = [_cache.allKeys mutableCopy]; + while (totalCount > maxCount || _totalCost > maxCost) { - //sort, oldest first - NSArray *keys = [[_cache allKeys] sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) { - RCTCacheEntry *entry1 = self.cache[key1]; - RCTCacheEntry *entry2 = self.cache[key2]; - return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber)); - }]; + NSInteger lowestSequenceNumber = INT_MAX; + RCTCacheEntry *lowestEntry = nil; + id lowestKey = nil; //remove oldest items until within limit for (id key in keys) { - if (totalCount <= maxCount && _totalCost <= maxCost) - { - break; - } RCTCacheEntry *entry = _cache[key]; - if (!_delegateRespondsToShouldEvictObject || [self.delegate cache:(RCTCache *)self shouldEvictObject:entry]) + if (entry.sequenceNumber < lowestSequenceNumber) + { + lowestSequenceNumber = entry.sequenceNumber; + lowestEntry = entry; + lowestKey = key; + } + } + + if (lowestKey) + { + [keys removeObject:lowestKey]; + if (!_delegateRespondsToShouldEvictObject || + [_delegate cache:(RCTCache *)self shouldEvictObject:lowestEntry.object]) { if (_delegateRespondsToWillEvictObject) { _currentlyCleaning = YES; - [self.delegate cache:(RCTCache *)self willEvictObject:entry]; + [self.delegate cache:(RCTCache *)self willEvictObject:lowestEntry.object]; _currentlyCleaning = NO; } - [_cache removeObjectForKey:key]; - _totalCost -= entry.cost; + [_cache removeObjectForKey:lowestKey]; + _totalCost -= lowestEntry.cost; totalCount --; + if (keepEntries) + { + [_entryPool addObject:lowestEntry]; + lowestEntry.object = nil; + } } } } @@ -161,8 +167,8 @@ { //sort, oldest first (in case we want to use that information in our eviction test) keys = [keys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) { - RCTCacheEntry *entry1 = self.cache[key1]; - RCTCacheEntry *entry2 = self.cache[key2]; + RCTCacheEntry *entry1 = self->_cache[key1]; + RCTCacheEntry *entry2 = self->_cache[key2]; return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber)); }]; } @@ -171,12 +177,12 @@ for (id key in keys) { RCTCacheEntry *entry = _cache[key]; - if (!_delegateRespondsToShouldEvictObject || [self.delegate cache:(RCTCache *)self shouldEvictObject:entry]) + if (!_delegateRespondsToShouldEvictObject || [_delegate cache:(RCTCache *)self shouldEvictObject:entry.object]) { if (_delegateRespondsToWillEvictObject) { _currentlyCleaning = YES; - [self.delegate cache:(RCTCache *)self willEvictObject:entry]; + [_delegate cache:(RCTCache *)self willEvictObject:entry.object]; _currentlyCleaning = NO; } [_cache removeObjectForKey:key]; @@ -208,7 +214,7 @@ } } -- (id)objectForKey:(id)key +- (id)objectForKey:(id)key { [_lock lock]; RCTCacheEntry *entry = _cache[key]; @@ -227,7 +233,7 @@ return [self objectForKey:key]; } -- (void)setObject:(id)obj forKey:(id)key +- (void)setObject:(id)obj forKey:(id)key { [self setObject:obj forKey:key cost:0]; } @@ -237,27 +243,44 @@ [self setObject:obj forKey:key cost:0]; } -- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g +- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g { + if (!obj) + { + [self removeObjectForKey:key]; + return; + } RCTAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method."); [_lock lock]; _totalCost -= [_cache[key] cost]; _totalCost += g; - _cache[key] = [RCTCacheEntry entryWithObject:obj cost:g sequenceNumber:_sequenceNumber++]; + RCTCacheEntry *entry = _cache[key]; + if (!entry) { + entry = [[RCTCacheEntry alloc] init]; + _cache[key] = entry; + } + entry.object = obj; + entry.cost = g; + entry.sequenceNumber = _sequenceNumber++; if (_sequenceNumber < 0) { [self resequence]; } [_lock unlock]; - [self cleanUp]; + [self cleanUp:YES]; } -- (void)removeObjectForKey:(id)key +- (void)removeObjectForKey:(id)key { RCTAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method."); [_lock lock]; - _totalCost -= [_cache[key] cost]; - [_cache removeObjectForKey:key]; + RCTCacheEntry *entry = _cache[key]; + if (entry) { + _totalCost -= entry.cost; + entry.object = nil; + [_entryPool addObject:entry]; + [_cache removeObjectForKey:key]; + } [_lock unlock]; } @@ -267,20 +290,42 @@ [_lock lock]; _totalCost = 0; _sequenceNumber = 0; + for (RCTCacheEntry *entry in _cache.allValues) + { + entry.object = nil; + [_entryPool addObject:entry]; + } [_cache removeAllObjects]; [_lock unlock]; } +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained [])buffer + count:(NSUInteger)len +{ + [_lock lock]; + NSUInteger count = [_cache countByEnumeratingWithState:state objects:buffer count:len]; + [_lock unlock]; + return count; +} + +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block +{ + [_lock lock]; + [_cache enumerateKeysAndObjectsUsingBlock:block]; + [_lock unlock]; +} + //handle unimplemented methods -- (BOOL)isKindOfClass:(Class)cls +- (BOOL)isKindOfClass:(Class)aClass { //pretend that we're an RCTCache if anyone asks - if (cls == [RCTCache class] || cls == [NSCache class]) + if (aClass == [RCTCache class] || aClass == [NSCache class]) { return YES; } - return [super isKindOfClass:cls]; + return [super isKindOfClass:aClass]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector @@ -308,19 +353,16 @@ @implementation RCTCache -@dynamic count; -@dynamic totalCost; - -+ (instancetype)alloc ++ (id)alloc { return (RCTCache *)[RCTCache_Private alloc]; } -- (id)objectForKeyedSubscript:(__unused NSNumber *)key -{ - return nil; -} - +- (id)objectForKeyedSubscript:(__unused id)key { return nil; } - (void)setObject:(__unused id)obj forKeyedSubscript:(__unused id)key {} +- (void)enumerateKeysAndObjectsUsingBlock:(__unused void (^)(id, id, BOOL *))block { } +- (NSUInteger)countByEnumeratingWithState:(__unused NSFastEnumerationState *)state + objects:(__unused __unsafe_unretained id [])buffer + count:(__unused NSUInteger)len { return 0; } @end diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 1b010accd..d4657ec74 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -91,7 +91,8 @@ typedef NSURL RCTFileURL; + (UIFont *)UIFont:(UIFont *)font withStyle:(id)json; + (UIFont *)UIFont:(UIFont *)font withFamily:(id)json; + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family - size:(id)size weight:(id)weight style:(id)style; + size:(id)size weight:(id)weight style:(id)style + scaleMultiplier:(CGFloat)scaleMultiplier; typedef NSArray NSStringArray; + (NSStringArray *)NSStringArray:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 06e080546..2ff271f64 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -396,7 +396,7 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ colorCache = [[RCTCache alloc] init]; - colorCache.countLimit = 1024; + colorCache.countLimit = 128; }); UIColor *color = colorCache[json]; if (color) { @@ -779,31 +779,33 @@ static BOOL RCTFontIsCondensed(UIFont *font) withFamily:json[@"fontFamily"] size:json[@"fontSize"] weight:json[@"fontWeight"] - style:json[@"fontStyle"]]; + style:json[@"fontStyle"] + scaleMultiplier:1.0f]; } + (UIFont *)UIFont:(UIFont *)font withSize:(id)json { - return [self UIFont:font withFamily:nil size:json weight:nil style:nil]; + return [self UIFont:font withFamily:nil size:json weight:nil style:nil scaleMultiplier:1.0]; } + (UIFont *)UIFont:(UIFont *)font withWeight:(id)json { - return [self UIFont:font withFamily:nil size:nil weight:json style:nil]; + return [self UIFont:font withFamily:nil size:nil weight:json style:nil scaleMultiplier:1.0]; } + (UIFont *)UIFont:(UIFont *)font withStyle:(id)json { - return [self UIFont:font withFamily:nil size:nil weight:nil style:json]; + return [self UIFont:font withFamily:nil size:nil weight:nil style:json scaleMultiplier:1.0]; } + (UIFont *)UIFont:(UIFont *)font withFamily:(id)json { - return [self UIFont:font withFamily:json size:nil weight:nil style:nil]; + return [self UIFont:font withFamily:json size:nil weight:nil style:nil scaleMultiplier:1.0]; } + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family size:(id)size weight:(id)weight style:(id)style + scaleMultiplier:(CGFloat)scaleMultiplier { // Defaults NSString *const RCTDefaultFontFamily = @"System"; @@ -828,6 +830,9 @@ static BOOL RCTFontIsCondensed(UIFont *font) // Get font attributes fontSize = [self CGFloat:size] ?: fontSize; + if (scaleMultiplier > 0.0 && scaleMultiplier != 1.0) { + fontSize = round(fontSize * scaleMultiplier); + } familyName = [self NSString:family] ?: familyName; isItalic = style ? [self RCTFontStyle:style] : isItalic; fontWeight = weight ? [self RCTFontWeight:weight] : fontWeight; diff --git a/React/Modules/RCTAccessibilityManager.h b/React/Modules/RCTAccessibilityManager.h new file mode 100644 index 000000000..03d22f945 --- /dev/null +++ b/React/Modules/RCTAccessibilityManager.h @@ -0,0 +1,27 @@ +/** + * 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. + */ + +#import + +#import "RCTBridgeModule.h" +#import "RCTBridge.h" + +extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; // posted when multiplier is changed + +@interface RCTAccessibilityManager : NSObject + +@property (nonatomic, readonly) CGFloat multiplier; + +@end + +@interface RCTBridge (RCTAccessibilityManager) + +@property (nonatomic, readonly) RCTAccessibilityManager *accessibilityManager; + +@end diff --git a/React/Modules/RCTAccessibilityManager.m b/React/Modules/RCTAccessibilityManager.m new file mode 100644 index 000000000..9271b1e3d --- /dev/null +++ b/React/Modules/RCTAccessibilityManager.m @@ -0,0 +1,144 @@ +/** + * 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. + */ + +#import "RCTAccessibilityManager.h" + +#import "RCTLog.h" + +NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification = @"RCTAccessibilityManagerDidUpdateMultiplierNotification"; + +@interface RCTAccessibilityManager () + +@property (nonatomic, copy) NSDictionary *multipliers; +@property (nonatomic, copy) NSString *contentSizeCategory; +@property (nonatomic, assign) CGFloat multiplier; + +@end + +@implementation RCTAccessibilityManager + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + ++ (NSDictionary *)JSToUIKitMap +{ + static NSDictionary *map = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + map = @{@"extraSmall": UIContentSizeCategoryExtraSmall, + @"small": UIContentSizeCategorySmall, + @"medium": UIContentSizeCategoryMedium, + @"large": UIContentSizeCategoryLarge, + @"extraLarge": UIContentSizeCategoryExtraLarge, + @"extraExtraLarge": UIContentSizeCategoryExtraExtraLarge, + @"extraExtraExtraLarge": UIContentSizeCategoryExtraExtraExtraLarge, + @"accessibilityMedium": UIContentSizeCategoryAccessibilityMedium, + @"accessibilityLarge": UIContentSizeCategoryAccessibilityLarge, + @"accessibilityExtraLarge": UIContentSizeCategoryAccessibilityExtraLarge, + @"accessibilityExtraExtraLarge": UIContentSizeCategoryAccessibilityExtraExtraLarge, + @"accessibilityExtraExtraExtraLarge": UIContentSizeCategoryAccessibilityExtraExtraExtraLarge}; + }); + return map; +} + ++ (NSString *)UIKitCategoryFromJSCategory:(NSString *)JSCategory +{ + return self.JSToUIKitMap[JSCategory]; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveNewContentSizeCategory:) + name:UIContentSizeCategoryDidChangeNotification + object:[UIApplication sharedApplication]]; + self.contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)didReceiveNewContentSizeCategory:(NSNotification *)note +{ + self.contentSizeCategory = note.userInfo[UIContentSizeCategoryNewValueKey]; +} + +- (void)setContentSizeCategory:(NSString *)contentSizeCategory +{ + if (_contentSizeCategory != contentSizeCategory) { + _contentSizeCategory = [contentSizeCategory copy]; + self.multiplier = [self multiplierForContentSizeCategory:_contentSizeCategory]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTAccessibilityManagerDidUpdateMultiplierNotification object:self]; + } +} + +- (CGFloat)multiplierForContentSizeCategory:(NSString *)category +{ + NSNumber *m = self.multipliers[category]; + if (m.doubleValue <= 0.0) { + RCTLogError(@"Can't determinte multiplier for category %@. Using 1.0.", category); + m = @1.0; + } + return m.doubleValue; +} + +- (NSDictionary *)multipliers +{ + if (_multipliers == nil) { + _multipliers = @{UIContentSizeCategoryExtraSmall: @0.823, + UIContentSizeCategorySmall: @0.882, + UIContentSizeCategoryMedium: @0.941, + UIContentSizeCategoryLarge: @1.0, + UIContentSizeCategoryExtraLarge: @1.118, + UIContentSizeCategoryExtraExtraLarge: @1.235, + UIContentSizeCategoryExtraExtraExtraLarge: @1.353, + UIContentSizeCategoryAccessibilityMedium: @1.786, + UIContentSizeCategoryAccessibilityLarge: @2.143, + UIContentSizeCategoryAccessibilityExtraLarge: @2.643, + UIContentSizeCategoryAccessibilityExtraExtraLarge: @3.143, + UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @3.571}; + } + return _multipliers; +} + +RCT_EXPORT_METHOD(setAccessibilityContentSizeMultipliers:(NSDictionary *)JSMultipliers) +{ + NSMutableDictionary *multipliers = [[NSMutableDictionary alloc] init]; + for (NSString *__nonnull JSCategory in JSMultipliers) { + NSNumber *m = JSMultipliers[JSCategory]; + NSString *UIKitCategory = [self.class UIKitCategoryFromJSCategory:JSCategory]; + multipliers[UIKitCategory] = m; + } + self.multipliers = multipliers; +} + +RCT_EXPORT_METHOD(getMultiplier:(RCTResponseSenderBlock)callback) +{ + if (callback) { + callback(@[ @(self.multiplier) ]); + } +} + +@end + +@implementation RCTBridge (RCTAccessibilityManager) + +- (RCTAccessibilityManager *)accessibilityManager +{ + return self.modules[RCTBridgeModuleNameForClass([RCTAccessibilityManager class])]; +} + +@end diff --git a/React/Modules/RCTSourceCode.m b/React/Modules/RCTSourceCode.m index 1b6eb842e..7498728f2 100644 --- a/React/Modules/RCTSourceCode.m +++ b/React/Modules/RCTSourceCode.m @@ -20,12 +20,12 @@ RCT_EXPORT_MODULE() @synthesize bridge = _bridge; RCT_EXPORT_METHOD(getScriptText:(RCTResponseSenderBlock)successCallback - failureCallback:(RCTResponseSenderBlock)failureCallback) + failureCallback:(RCTResponseErrorBlock)failureCallback) { if (self.scriptText && self.scriptURL) { successCallback(@[@{@"text": self.scriptText, @"url":[self.scriptURL absoluteString]}]); } else { - failureCallback(@[RCTMakeError(@"Source code is not available", nil, nil)]); + failureCallback(RCTErrorWithMessage(@"Source code is not available")); } } diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index cbd7c167f..1efedc510 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -14,6 +14,12 @@ #import "RCTInvalidating.h" #import "RCTViewManager.h" +/** + * Posted right before re-render happens. This is a chance for views to invalidate their state so + * next render cycle will pick up updated views and layout appropriately. + */ +extern NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification; + @protocol RCTScrollableProtocol; /** diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index f526f5f8c..39cd38a21 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -14,6 +14,7 @@ #import #import "Layout.h" +#import "RCTAccessibilityManager.h" #import "RCTAnimationType.h" #import "RCTAssert.h" #import "RCTBridge.h" @@ -35,6 +36,8 @@ static void RCTTraverseViewNodes(id view, void (^block)(id)); static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps); +NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification = @"RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification"; + @interface RCTAnimation : NSObject @property (nonatomic, readonly) NSTimeInterval duration; @@ -262,10 +265,33 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass) _rootViewTags = [[NSMutableSet alloc] init]; _bridgeTransactionListeners = [[NSMutableSet alloc] init]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveNewContentSizeMultiplier) + name:RCTAccessibilityManagerDidUpdateMultiplierNotification + object:nil]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)didReceiveNewContentSizeMultiplier +{ + __weak RCTUIManager *weakSelf = self; + dispatch_async(self.methodQueue, ^{ + __weak RCTUIManager *strongSelf = weakSelf; + if (strongSelf) { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification + object:strongSelf]; + [strongSelf batchDidComplete]; + } + }); +} + - (BOOL)isValid { return _viewRegistry != nil; diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 5e0434b30..d0bbda232 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */; }; 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */; }; 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBACB1A6023D300E9B192 /* RCTConvert.m */; }; + E9B20B7B1B500126007A2DA7 /* RCTAccessibilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -239,6 +240,8 @@ 83CBBACA1A6023D300E9B192 /* RCTConvert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTConvert.h; sourceTree = ""; }; 83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = ""; }; E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = ""; }; + E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAccessibilityManager.h; sourceTree = ""; }; + E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAccessibilityManager.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -275,6 +278,8 @@ 13B07FE01A69315300A75B9A /* Modules */ = { isa = PBXGroup; children = ( + E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */, + E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */, 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */, 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */, 1372B7081AB030C200659ED6 /* RCTAppState.h */, @@ -567,6 +572,7 @@ 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */, 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */, 13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */, + E9B20B7B1B500126007A2DA7 /* RCTAccessibilityManager.m in Sources */, 13B07FF21A69327A00A75B9A /* RCTTiming.m in Sources */, 1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */, 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */, diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 2c350b18b..e5cccbd3f 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -439,12 +439,15 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st @"flex", }; layoutKeys = [NSSet setWithObjects:layoutKeyStrings count:sizeof(layoutKeyStrings)/sizeof(*layoutKeyStrings)]; + // layoutKeys are the only keys whose presence does not reject layout-only status. static NSString *const specialKeyStrings[] = { @"accessible", @"collapsible", }; specialKeys = [NSSet setWithObjects:specialKeyStrings count:sizeof(specialKeyStrings)/sizeof(*specialKeyStrings)]; + // specialKeys are keys whose presence does not indicate whether layout-only or not + // their values must be tested below } NSNumber *collapsible = self.allProps[@"collapsible"]; diff --git a/React/Views/RCTTabBar.h b/React/Views/RCTTabBar.h index 6f491ca08..5c24b9039 100644 --- a/React/Views/RCTTabBar.h +++ b/React/Views/RCTTabBar.h @@ -15,6 +15,7 @@ @property (nonatomic, strong) UIColor *tintColor; @property (nonatomic, strong) UIColor *barTintColor; +@property (nonatomic, assign) BOOL translucent; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 06df7bad6..a4b55975c 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -140,6 +140,14 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) _tabController.tabBar.tintColor = tintColor; } +- (BOOL)translucent { + return _tabController.tabBar.isTranslucent; +} + +- (void)setTranslucent:(BOOL)translucent { + _tabController.tabBar.translucent = translucent; +} + #pragma mark - UITabBarControllerDelegate - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController diff --git a/React/Views/RCTTabBarManager.m b/React/Views/RCTTabBarManager.m index 2290c78c7..7b9616246 100644 --- a/React/Views/RCTTabBarManager.m +++ b/React/Views/RCTTabBarManager.m @@ -23,5 +23,6 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL) @end diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 51d060ca3..6e7019f58 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -64,7 +64,7 @@ typedef void (^RCTViewEventHandler)(RCTView *view); @property (nonatomic, assign) CGFloat borderBottomRightRadius; /** - * Border colors. + * Border colors (actually retained). */ @property (nonatomic, assign) CGColorRef borderTopColor; @property (nonatomic, assign) CGColorRef borderRightColor; diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 1acb1b2d6..407eba24f 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -480,7 +480,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:unused) _borderTopColor ?: _borderColor, _borderLeftColor ?: _borderColor, _borderBottomColor ?: _borderColor, - _borderRightColor ?: _borderColor + _borderRightColor ?: _borderColor, }; } @@ -580,14 +580,15 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:unused) #pragma mark Border Color -#define setBorderColor(side) \ - - (void)setBorder##side##Color:(CGColorRef)border##side##Color \ - { \ - if (CGColorEqualToColor(_border##side##Color, border##side##Color)) { \ - return; \ - } \ - _border##side##Color = border##side##Color; \ - [self.layer setNeedsDisplay]; \ +#define setBorderColor(side) \ + - (void)setBorder##side##Color:(CGColorRef)color \ + { \ + if (CGColorEqualToColor(_border##side##Color, color)) { \ + return; \ + } \ + CGColorRelease(_border##side##Color); \ + _border##side##Color = CGColorRetain(color); \ + [self.layer setNeedsDisplay]; \ } setBorderColor() @@ -598,14 +599,14 @@ setBorderColor(Left) #pragma mark - Border Width -#define setBorderWidth(side) \ - - (void)setBorder##side##Width:(CGFloat)border##side##Width \ - { \ - if (_border##side##Width == border##side##Width) { \ - return; \ - } \ - _border##side##Width = border##side##Width; \ - [self.layer setNeedsDisplay]; \ +#define setBorderWidth(side) \ + - (void)setBorder##side##Width:(CGFloat)width \ + { \ + if (_border##side##Width == width) { \ + return; \ + } \ + _border##side##Width = width; \ + [self.layer setNeedsDisplay]; \ } setBorderWidth() @@ -614,14 +615,14 @@ setBorderWidth(Right) setBorderWidth(Bottom) setBorderWidth(Left) -#define setBorderRadius(side) \ - - (void)setBorder##side##Radius:(CGFloat)border##side##Radius \ - { \ - if (_border##side##Radius == border##side##Radius) { \ - return; \ - } \ - _border##side##Radius = border##side##Radius; \ - [self.layer setNeedsDisplay]; \ +#define setBorderRadius(side) \ + - (void)setBorder##side##Radius:(CGFloat)radius \ + { \ + if (_border##side##Radius == radius) { \ + return; \ + } \ + _border##side##Radius = radius; \ + [self.layer setNeedsDisplay]; \ } setBorderRadius() @@ -630,4 +631,13 @@ setBorderRadius(TopRight) setBorderRadius(BottomLeft) setBorderRadius(BottomRight) +- (void)dealloc +{ + CGColorRelease(_borderColor); + CGColorRelease(_borderTopColor); + CGColorRelease(_borderRightColor); + CGColorRelease(_borderBottomColor); + CGColorRelease(_borderLeftColor); +} + @end diff --git a/package.json b/package.json index 2b8b33496..c70eda5e0 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "absolute-path": "0.0.0", "babel": "5.4.3", "babel-core": "^5.6.4", - "bluebird": "^2.9.21", "chalk": "^1.0.0", "connect": "2.8.3", "debug": "~2.1.0",