From 875ab073ea0b78fe14ccd6e77272e5747dc66db5 Mon Sep 17 00:00:00 2001 From: Nathan Spaun Date: Mon, 13 Jul 2015 18:17:35 -0700 Subject: [PATCH 01/10] [Treehouse] Sync from android permalink keyboard layout changes --- Libraries/Components/ScrollResponder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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() { From eb066597115d64e3b153bbf6e3e560fdcbf9e2f6 Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Tue, 14 Jul 2015 01:59:28 -0700 Subject: [PATCH 02/10] [React Native] Update code comments about layout-only view --- Libraries/Components/View/View.js | 6 ++++++ Libraries/ReactNative/ReactNativeViewAttributes.js | 6 +++++- React/Views/RCTShadowView.m | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) 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/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/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"]; From be2cabc3f89b22aa8624c476e48598274ca5dd75 Mon Sep 17 00:00:00 2001 From: Vladislav Alexeev Date: Tue, 14 Jul 2015 03:12:15 -0700 Subject: [PATCH 03/10] Dynamic Text Sizes for Text component Summary: Dynamic Text Sizes for Text component. Text gains new prop - allowFontScaling (true by default). There is also AccessibilityManager module that allows you to tune multipliers per each content size category, but predefined multipliers are there. This could potentially break some apps so please test carefully. --- Examples/UIExplorer/TextExample.ios.js | 19 ++++ Libraries/ART/RCTConvert+ART.m | 2 +- Libraries/Text/RCTShadowRawText.m | 24 ++++ Libraries/Text/RCTShadowText.h | 2 + Libraries/Text/RCTShadowText.m | 49 +++++++- Libraries/Text/RCTTextManager.m | 3 + Libraries/Text/Text.js | 17 ++- React/Base/RCTConvert.h | 3 +- React/Base/RCTConvert.m | 15 ++- React/Modules/RCTAccessibilityManager.h | 27 +++++ React/Modules/RCTAccessibilityManager.m | 144 ++++++++++++++++++++++++ React/Modules/RCTUIManager.h | 6 + React/Modules/RCTUIManager.m | 26 +++++ React/React.xcodeproj/project.pbxproj | 6 + 14 files changed, 331 insertions(+), 12 deletions(-) create mode 100644 React/Modules/RCTAccessibilityManager.h create mode 100644 React/Modules/RCTAccessibilityManager.m 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/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/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..2305a1217 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -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/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 */, From a65bbe14d36d487529afdc69f7479d9f5d90bbca Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Tue, 14 Jul 2015 03:51:22 -0700 Subject: [PATCH 04/10] [React Native] Update additional callsites with RCTResponseErrorBlock --- React/Modules/RCTSourceCode.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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")); } } From b34a85f4da87c62656ca9d29c5085a46e768d147 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 14 Jul 2015 04:06:17 -0700 Subject: [PATCH 05/10] Merged RCTStaticImage with FB internal version Summary: Merged RCTStaticImage with our internal RKStaticImage and ported over logic where assets are loaded at the optimal size and reloaded if the view size changes. --- .../Image/RCTImage.xcodeproj/project.pbxproj | 6 + Libraries/Image/RCTImageDownloader.m | 80 +--------- Libraries/Image/RCTImageLoader.h | 23 ++- Libraries/Image/RCTImageLoader.m | 90 ++++++++--- Libraries/Image/RCTImageUtils.h | 38 +++++ Libraries/Image/RCTImageUtils.m | 147 ++++++++++++++++++ Libraries/Image/RCTStaticImage.h | 1 + Libraries/Image/RCTStaticImage.m | 57 +++++++ Libraries/Image/RCTStaticImageManager.m | 37 +---- 9 files changed, 342 insertions(+), 137 deletions(-) create mode 100644 Libraries/Image/RCTImageUtils.h create mode 100644 Libraries/Image/RCTImageUtils.m 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 From 0c61b49f0aca41f29ea886d310a5ae851944bbe3 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 14 Jul 2015 04:53:54 -0700 Subject: [PATCH 06/10] Improved RCTCache performance + fixed border color crash Summary: RCTCache had really bad insertion performance when the cache was full due to having to LRU-sort the entries. This was making color animations very slow. I've fixed this in two ways: 1) by removing the sort and doing a linear search to remove old entries, which changes insertion perf to O(n) in the worst case instead of O(n log n) or even (n2). 2) by reducing the size of the color cache to 128 from 1024, which should be fine for normal use, without penalising animation performance. Separately, border colors were not being retained, which caused crashes when the color cache was cleared. I've fixed that by retaining the border colors inside RCTView. --- React/Base/RCTCache.h | 7 +- React/Base/RCTCache.m | 162 +++++++++++++++++++++++++--------------- React/Base/RCTConvert.m | 2 +- React/Views/RCTView.h | 2 +- React/Views/RCTView.m | 60 ++++++++------- 5 files changed, 145 insertions(+), 88 deletions(-) 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.m b/React/Base/RCTConvert.m index 2305a1217..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) { 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 From 691a1dafd1960f582e397f01466beaec8e069022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Tue, 14 Jul 2015 07:44:04 -0700 Subject: [PATCH 07/10] s/RKScrollViewManager/RCTScrollViewManager --- Libraries/CustomComponents/ListView/ListView.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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, ); From 9936a2406d4c886abfeca50b2c8527e354ab654e Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Tue, 14 Jul 2015 08:05:08 -0700 Subject: [PATCH 08/10] [TabBarIOS] Add translucent property to TabBarIOS Summary: To be on par with NavigatorIOS, I added the translucent property to TabBarIOS. Usage: ``` ``` Closes https://github.com/facebook/react-native/pull/1937 Github Author: Jean Regisser --- Libraries/Components/TabBarIOS/TabBarIOS.ios.js | 9 +++++++-- React/Views/RCTTabBar.h | 1 + React/Views/RCTTabBar.m | 8 ++++++++ React/Views/RCTTabBarManager.m | 1 + 4 files changed, 17 insertions(+), 2 deletions(-) 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/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 From a8cb47e0118219fa66216cd230912fd82465eb5d Mon Sep 17 00:00:00 2001 From: Jacob Rosenthal Date: Tue, 14 Jul 2015 09:07:24 -0700 Subject: [PATCH 09/10] add local notification api schedule and present Summary: Add local notifications to the push library. ``` var PushNotificationIOS = React.PushNotificationIOS; PushNotificationIOS.requestPermissions(); var notification = { "fireDate": Date.now() + 10000, "alertBody":"Whats up pumpkin" }; PushNotificationIOS.scheduleLocalNotification(notification); //lock screen or move away from app ``` Apple has another api for pushing immediately instead of scheduling, like when your background delegate has been called with some new data (bluetooth, location, etc) ``` var PushNotificationIOS = React.PushNotificationIOS; PushNotificationIOS.requestPermissions(); var notification = { "alertBody":"Whats up pumpkin" }; PushNotificationIOS.presentLocalNotification(notification); //lock screen or move away from app ``` Closed https://github.com/facebook/react-native/pull/843 looks related: See https://developer.apple.com/library/ios/documentation/iPhone/Reference/UILocalNotification_Class/ for much more available in Closes https://github.com/facebook/react-native/pull/1616 Github Author: Jacob Rosenthal --- .../PushNotificationIOS.js | 25 ++++++++++++++++ .../RCTPushNotificationManager.m | 29 +++++++++++++++++++ 2 files changed, 54 insertions(+) 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 From ebd046ae217ce0b00b8b6540ee65b4d764777bcc Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Tue, 14 Jul 2015 10:03:48 -0700 Subject: [PATCH 10/10] Remove bluebird --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index cfc1367ec..822c730ea 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",