diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 6d86930b7..87f49232b 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -312,6 +312,12 @@ var NavigatorIOS = React.createClass({ this.navigationContext = new NavigationContext(); }, + getDefaultProps: function(): Object { + return { + translucent: true, + }; + }, + getInitialState: function(): State { return { idStack: [getuid()], @@ -591,37 +597,26 @@ var NavigatorIOS = React.createClass({ }, _routeToStackItem: function(route: Route, i: number) { - var Component = route.component; - var shouldUpdateChild = this.state.updatingAllIndicesAtOrBeyond != null && + var {component, wrapperStyle, passProps, ...route} = route; + var {itemWrapperStyle, ...props} = this.props; + var shouldUpdateChild = + this.state.updatingAllIndicesAtOrBeyond && this.state.updatingAllIndicesAtOrBeyond >= i; - + var Component = component; return ( + itemWrapperStyle, + wrapperStyle + ]}> diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js index 20e0cf624..f3fb6964c 100644 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js @@ -16,7 +16,6 @@ var React = require('React'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); var View = require('View'); -var resolveAssetSource = require('resolveAssetSource'); var requireNativeComponent = require('requireNativeComponent'); @@ -52,10 +51,7 @@ var TabBarItemIOS = React.createClass({ /** * A custom icon for the tab. It is ignored when a system icon is defined. */ - icon: React.PropTypes.oneOfType([ - React.PropTypes.string, - Image.propTypes.source, - ]), + icon: Image.propTypes.source, /** * A custom icon when the tab is selected. It is ignored when a system * icon is defined. If left empty, the icon will be tinted in blue. @@ -101,29 +97,23 @@ var TabBarItemIOS = React.createClass({ }, render: function() { - var tabContents = null; + var {style, children, ...props} = this.props; + // if the tab has already been shown once, always continue to show it so we // preserve state between tab transitions if (this.state.hasBeenSelected) { - tabContents = + var tabContents = - {this.props.children} + {children} ; } else { - tabContents = ; + var tabContents = ; } - - var badge = typeof this.props.badge === 'number' ? - '' + this.props.badge : - this.props.badge; - + return ( + {...props} + style={[styles.tab, style]}> {tabContents} ); diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 6943bf909..2b649bf9c 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -69,13 +69,16 @@ var Image = React.createClass({ PropTypes.number, ]), /** - * A static image to display while downloading the final image off the - * network. + * A static image to display while loading the image source. * @platform ios */ - defaultSource: PropTypes.shape({ - uri: PropTypes.string, - }), + defaultSource: PropTypes.oneOfType([ + PropTypes.shape({ + uri: PropTypes.string, + }), + // Opaque type returned by require('./image.jpg') + PropTypes.number, + ]), /** * When true, indicates the image is an accessibility element. * @platform ios @@ -155,23 +158,10 @@ var Image = React.createClass({ }, render: function() { - for (var prop in cfg.nativeOnly) { - if (this.props[prop] !== undefined) { - console.warn('Prop `' + prop + ' = ' + this.props[prop] + '` should ' + - 'not be set directly on Image.'); - } - } var source = resolveAssetSource(this.props.source) || {}; - var defaultSource = (this.props.defaultSource && resolveAssetSource(this.props.defaultSource)) || {}; - var {width, height} = source; var style = flattenStyle([{width, height}, styles.base, this.props.style]) || {}; - if (source.uri === '') { - console.warn('source.uri should not be an empty string'); - return ; - } - var isNetwork = source.uri && source.uri.match(/^https?:/); var RawImage = isNetwork ? RCTNetworkImageView : RCTImageView; var resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108 @@ -192,8 +182,7 @@ var Image = React.createClass({ style={style} resizeMode={resizeMode} tintColor={tintColor} - src={source.uri} - defaultImageSrc={defaultSource.uri} + source={source} /> ); } @@ -206,16 +195,8 @@ var styles = StyleSheet.create({ }, }); -var cfg = { - nativeOnly: { - src: true, - defaultImageSrc: true, - imageTag: true, - progressHandlerRegistered: true, - }, -}; -var RCTImageView = requireNativeComponent('RCTImageView', Image, cfg); -var RCTNetworkImageView = NativeModules.NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image, cfg) : RCTImageView; +var RCTImageView = requireNativeComponent('RCTImageView', Image); +var RCTNetworkImageView = NativeModules.NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView; var RCTVirtualImage = requireNativeComponent('RCTVirtualImage', Image); module.exports = Image; diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index 3146a5b56..a6c2ccd46 100644 --- a/Libraries/Image/RCTImageUtils.m +++ b/Libraries/Image/RCTImageUtils.m @@ -231,6 +231,11 @@ UIImage *RCTDecodeImageWithData(NSData *data, if (CGSizeEqualToSize(destSize, CGSizeZero)) { destSize = sourceSize; + if (!destScale) { + destScale = 1; + } + } else if (!destScale) { + destScale = RCTScreenScale(); } // calculate target size @@ -253,13 +258,9 @@ UIImage *RCTDecodeImageWithData(NSData *data, return nil; } - // adjust scale - size_t actualWidth = CGImageGetWidth(imageRef); - CGFloat scale = actualWidth / targetSize.width * destScale; - // return image UIImage *image = [UIImage imageWithCGImage:imageRef - scale:scale + scale:destScale orientation:UIImageOrientationUp]; CGImageRelease(imageRef); return image; diff --git a/Libraries/Image/RCTImageView.h b/Libraries/Image/RCTImageView.h index a561c0f7f..8b5b993a7 100644 --- a/Libraries/Image/RCTImageView.h +++ b/Libraries/Image/RCTImageView.h @@ -11,6 +11,7 @@ #import "RCTImageComponent.h" @class RCTBridge; +@class RCTImageSource; @interface RCTImageView : UIImageView @@ -19,6 +20,6 @@ @property (nonatomic, assign) UIEdgeInsets capInsets; @property (nonatomic, strong) UIImage *defaultImage; @property (nonatomic, assign) UIImageRenderingMode renderingMode; -@property (nonatomic, copy) NSString *src; +@property (nonatomic, strong) RCTImageSource *source; @end diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 246220840..656dd61e8 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -13,6 +13,7 @@ #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTImageLoader.h" +#import "RCTImageSource.h" #import "RCTImageUtils.h" #import "RCTUtils.h" @@ -107,6 +108,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) } } +- (void)setTintColor:(UIColor *)tintColor +{ + super.tintColor = tintColor; + self.renderingMode = tintColor ? UIImageRenderingModeAlwaysTemplate : UIImageRenderingModeAlwaysOriginal; +} + - (void)setRenderingMode:(UIImageRenderingMode)renderingMode { if (_renderingMode != renderingMode) { @@ -115,28 +122,29 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) } } -- (void)setSrc:(NSString *)src +- (void)setSource:(RCTImageSource *)source { - if (![src isEqual:_src]) { - _src = [src copy]; + if (![source isEqual:_source]) { + _source = source; [self reloadImage]; } } -+ (BOOL)srcNeedsReload:(NSString *)src +- (BOOL)sourceNeedsReload { + NSString *scheme = _source.imageURL.scheme; return - [src hasPrefix:@"http://"] || - [src hasPrefix:@"https://"] || - [src hasPrefix:@"assets-library://"] || - [src hasPrefix:@"ph://"]; + [scheme isEqualToString:@"http"] || + [scheme isEqualToString:@"https"] || + [scheme isEqualToString:@"assets-library"] || + [scheme isEqualToString:@"ph"]; } - (void)setContentMode:(UIViewContentMode)contentMode { if (self.contentMode != contentMode) { super.contentMode = contentMode; - if ([RCTImageView srcNeedsReload:_src]) { + if ([self sourceNeedsReload]) { [self reloadImage]; } } @@ -162,7 +170,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) { [self cancelImageLoad]; - if (_src && self.frame.size.width > 0 && self.frame.size.height > 0) { + if (_source && self.frame.size.width > 0 && self.frame.size.height > 0) { if (_onLoadStart) { _onLoadStart(nil); } @@ -177,31 +185,37 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) }; } - _reloadImageCancellationBlock = [_bridge.imageLoader loadImageWithTag:_src + RCTImageSource *source = _source; + __weak RCTImageView *weakSelf = self; + _reloadImageCancellationBlock = [_bridge.imageLoader loadImageWithTag:_source.imageURL.absoluteString size:self.bounds.size scale:RCTScreenScale() resizeMode:self.contentMode progressBlock:progressHandler completionBlock:^(NSError *error, UIImage *image) { - if (error) { - if (_onError) { - _onError(@{ @"error": error.localizedDescription }); - } - } else { - if (_onLoad) { - _onLoad(nil); - } - } - if (_onLoadEnd) { - _onLoadEnd(nil); - } - dispatch_async(dispatch_get_main_queue(), ^{ + RCTImageView *strongSelf = weakSelf; + if (![source isEqual:strongSelf.source]) { + // Bail out if source has changed since we started loading + return; + } if (image.reactKeyframeAnimation) { - [self.layer addAnimation:image.reactKeyframeAnimation forKey:@"contents"]; + [strongSelf.layer addAnimation:image.reactKeyframeAnimation forKey:@"contents"]; } else { - [self.layer removeAnimationForKey:@"contents"]; - self.image = image; + [strongSelf.layer removeAnimationForKey:@"contents"]; + strongSelf.image = image; + } + if (error) { + if (strongSelf->_onError) { + strongSelf->_onError(@{ @"error": error.localizedDescription }); + } + } else { + if (strongSelf->_onLoad) { + strongSelf->_onLoad(nil); + } + } + if (strongSelf->_onLoadEnd) { + strongSelf->_onLoadEnd(nil); } }); }]; @@ -217,13 +231,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) if (!self.image || self.image == _defaultImage) { _targetSize = frame.size; [self reloadImage]; - } else if ([RCTImageView srcNeedsReload:_src]) { + } else if ([self sourceNeedsReload]) { CGSize imageSize = self.image.size; CGSize idealSize = RCTTargetSize(imageSize, self.image.scale, frame.size, RCTScreenScale(), self.contentMode, YES); if (RCTShouldReloadImageForSizeChange(imageSize, idealSize)) { if (RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) { - RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _src, NSStringFromCGSize(idealSize)); + RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _source.imageURL, NSStringFromCGSize(idealSize)); // If the existing image or an image being loaded are not the right size, reload the asset in case there is a // better size available. diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m index bcf2f0bdb..c20678895 100644 --- a/Libraries/Image/RCTImageViewManager.m +++ b/Libraries/Image/RCTImageViewManager.m @@ -12,6 +12,7 @@ #import #import "RCTConvert.h" +#import "RCTImageSource.h" #import "RCTImageView.h" @implementation RCTImageViewManager @@ -24,23 +25,14 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets) -RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage) -RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) -RCT_EXPORT_VIEW_PROPERTY(src, NSString) +RCT_REMAP_VIEW_PROPERTY(defaultSource, defaultImage, UIImage) RCT_EXPORT_VIEW_PROPERTY(onLoadStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, RCTDirectEventBlock) -RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView) -{ - if (json) { - view.renderingMode = UIImageRenderingModeAlwaysTemplate; - view.tintColor = [RCTConvert UIColor:json]; - } else { - view.renderingMode = defaultView.renderingMode; - view.tintColor = defaultView.tintColor; - } -} +RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) +RCT_EXPORT_VIEW_PROPERTY(source, RCTImageSource) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) @end diff --git a/Libraries/Image/RCTShadowVirtualImage.h b/Libraries/Image/RCTShadowVirtualImage.h index 841c215e2..bd8bb51ae 100644 --- a/Libraries/Image/RCTShadowVirtualImage.h +++ b/Libraries/Image/RCTShadowVirtualImage.h @@ -9,6 +9,7 @@ #import "RCTShadowView.h" #import "RCTImageComponent.h" +#import "RCTImageSource.h" @class RCTBridge; @@ -20,6 +21,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge; -@property (nonatomic, copy) NSDictionary *source; +@property (nonatomic, strong) RCTImageSource *source; @end diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m index 431fbac07..f3486f9a2 100644 --- a/Libraries/Image/RCTShadowVirtualImage.m +++ b/Libraries/Image/RCTShadowVirtualImage.m @@ -16,6 +16,7 @@ @implementation RCTShadowVirtualImage { RCTBridge *_bridge; + RCTImageLoaderCancellationBlock _cancellationBlock; } @synthesize image = _image; @@ -30,23 +31,31 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) -- (void)setSource:(NSDictionary *)source +- (void)setSource:(RCTImageSource *)source { if (![source isEqual:_source]) { - _source = [source copy]; - NSString *imageTag = [RCTConvert NSString:_source[@"uri"]]; - CGFloat scale = [RCTConvert CGFloat:_source[@"scale"]] ?: 1; + + // Cancel previous request + if (_cancellationBlock) { + _cancellationBlock(); + } + + _source = source; __weak RCTShadowVirtualImage *weakSelf = self; - [_bridge.imageLoader loadImageWithTag:imageTag - size:CGSizeZero - scale:scale - resizeMode:UIViewContentModeScaleToFill - progressBlock:nil - completionBlock:^(NSError *error, UIImage *image) { + _cancellationBlock = [_bridge.imageLoader loadImageWithTag:source.imageURL.absoluteString + size:source.size + scale:source.scale + resizeMode:UIViewContentModeScaleToFill + progressBlock:nil + completionBlock:^(NSError *error, UIImage *image) { dispatch_async(_bridge.uiManager.methodQueue, ^{ RCTShadowVirtualImage *strongSelf = weakSelf; + if (![source isEqual:strongSelf.source]) { + // Bail out if source has changed since we started loading + return; + } strongSelf->_image = image; [strongSelf dirtyText]; }); @@ -54,4 +63,11 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) } } +- (void)dealloc +{ + if (_cancellationBlock) { + _cancellationBlock(); + } +} + @end diff --git a/Libraries/Image/RCTVirtualImageManager.m b/Libraries/Image/RCTVirtualImageManager.m index 2800a92b0..d497026ab 100644 --- a/Libraries/Image/RCTVirtualImageManager.m +++ b/Libraries/Image/RCTVirtualImageManager.m @@ -19,6 +19,6 @@ RCT_EXPORT_MODULE() return [[RCTShadowVirtualImage alloc] initWithBridge:self.bridge]; } -RCT_EXPORT_SHADOW_PROPERTY(source, NSDictionary) +RCT_EXPORT_SHADOW_PROPERTY(source, RCTImageSource) @end diff --git a/Libraries/ReactIOS/requireNativeComponent.js b/Libraries/ReactIOS/requireNativeComponent.js index a046fcbd1..84789e6cf 100644 --- a/Libraries/ReactIOS/requireNativeComponent.js +++ b/Libraries/ReactIOS/requireNativeComponent.js @@ -16,10 +16,12 @@ var UIManager = require('UIManager'); var UnimplementedView = require('UnimplementedView'); var createReactNativeComponentClass = require('createReactNativeComponentClass'); + var insetsDiffer = require('insetsDiffer'); var pointsDiffer = require('pointsDiffer'); var matricesDiffer = require('matricesDiffer'); var processColor = require('processColor'); +var resolveAssetSource = require('resolveAssetSource'); var sizesDiffer = require('sizesDiffer'); var verifyPropTypes = require('verifyPropTypes'); var warning = require('warning'); @@ -110,6 +112,9 @@ var TypeToProcessorMap = { CGColorArray: processColor, UIColor: processColor, UIColorArray: processColor, + CGImage: resolveAssetSource, + UIImage: resolveAssetSource, + RCTImageSource: resolveAssetSource, // Android Types Color: processColor, }; diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 3d1c4291d..6347cc9ca 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -85,9 +85,6 @@ typedef NSURL RCTFileURL; + (UIColor *)UIColor:(id)json; + (CGColorRef)CGColor:(id)json CF_RETURNS_NOT_RETAINED; -+ (UIImage *)UIImage:(id)json; -+ (CGImageRef)CGImage:(id)json CF_RETURNS_NOT_RETAINED; - + (UIFont *)UIFont:(id)json; + (UIFont *)UIFont:(UIFont *)font withSize:(id)json; + (UIFont *)UIFont:(UIFont *)font withWeight:(id)json; @@ -146,6 +143,18 @@ typedef BOOL css_clip_t, css_backface_visibility_t; @end +@interface RCTConvert (Deprecated) + +/** + * Synchronous image loading is generally a bad idea for performance reasons. + * If you need to pass image references, try to use `RCTImageSource` and then + * `RCTImageLoader` instead of converting directly to a UIImage. + */ ++ (UIImage *)UIImage:(id)json; ++ (CGImageRef)CGImage:(id)json CF_RETURNS_NOT_RETAINED; + +@end + /** * Underlying implementations of RCT_XXX_CONVERTER macros. Ignore these. */ diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index b2c033b38..244fe6c0a 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -12,6 +12,7 @@ #import #import "RCTDefines.h" +#import "RCTImageSource.h" #import "RCTUtils.h" @implementation RCTConvert @@ -421,7 +422,7 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ CGFloat b = (argb & 0xFF) / 255.0; return [UIColor colorWithRed:r green:g blue:b alpha:a]; } else { - RCTLogConvertError(json, @"a color"); + RCTLogConvertError(json, @"a UIColor. Did you forget to call processColor() on the JS side?"); return nil; } } @@ -431,78 +432,6 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ return [self UIColor:json].CGColor; } -/* This method is only used when loading images synchronously, e.g. for tabbar icons */ -+ (UIImage *)UIImage:(id)json -{ - // TODO: we might as well cache the result of these checks (and possibly the - // image itself) so as to reduce overhead on subsequent checks of the same input - - if (!json) { - return nil; - } - - __block UIImage *image; - if (![NSThread isMainThread]) { - // It seems that none of the UIImage loading methods can be guaranteed - // thread safe, so we'll pick the lesser of two evils here and block rather - // than run the risk of crashing - RCTLogWarn(@"Calling [RCTConvert UIImage:] on a background thread is not recommended"); - dispatch_sync(dispatch_get_main_queue(), ^{ - image = [self UIImage:json]; - }); - return image; - } - - NSString *path; - CGFloat scale = 0.0; - BOOL isPackagerAsset = NO; - if ([json isKindOfClass:[NSString class]]) { - path = json; - } else if ([json isKindOfClass:[NSDictionary class]]) { - if (!(path = [self NSString:json[@"uri"]])) { - return nil; - } - scale = [self CGFloat:json[@"scale"]]; - isPackagerAsset = [self BOOL:json[@"__packager_asset"]]; - } else { - RCTLogConvertError(json, @"an image"); - return nil; - } - - NSURL *URL = [self NSURL:path]; - NSString *scheme = URL.scheme.lowercaseString; - if ([scheme isEqualToString:@"file"]) { - NSString *assetName = RCTBundlePathForURL(URL); - image = [UIImage imageNamed:assetName]; - if (!image) { - // Attempt to load from the file system - NSString *filePath = URL.path; - if (filePath.pathExtension.length == 0) { - filePath = [filePath stringByAppendingPathExtension:@"png"]; - } - image = [UIImage imageWithContentsOfFile:filePath]; - } - } else if ([scheme isEqualToString:@"data"]) { - image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; - } else if ([scheme isEqualToString:@"http"] && isPackagerAsset) { - image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; - } else { - RCTLogConvertError(json, @"an image. Only local files or data URIs are supported"); - } - - if (scale > 0) { - image = [UIImage imageWithCGImage:image.CGImage - scale:scale - orientation:image.imageOrientation]; - } - return image; -} - -+ (CGImageRef)CGImage:(id)json -{ - return [self UIImage:json].CGImage; -} - #if !defined(__IPHONE_8_2) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_2 // These constants are defined in iPhone SDK 8.2, but the app cannot run on @@ -860,3 +789,84 @@ RCT_ENUM_CONVERTER(RCTAnimationType, (@{ }), RCTAnimationTypeEaseInEaseOut, integerValue) @end + +@interface RCTImageSource (Packager) + +@property (nonatomic, assign) BOOL packagerAsset; + +@end + +@implementation RCTConvert (Deprecated) + +/* This method is only used when loading images synchronously, e.g. for tabbar icons */ ++ (UIImage *)UIImage:(id)json +{ + if (!json) { + return nil; + } + + RCTImageSource *imageSource = [self RCTImageSource:json]; + if (!imageSource) { + return nil; + } + + __block UIImage *image; + if (![NSThread isMainThread]) { + // It seems that none of the UIImage loading methods can be guaranteed + // thread safe, so we'll pick the lesser of two evils here and block rather + // than run the risk of crashing + RCTLogWarn(@"Calling [RCTConvert UIImage:] on a background thread is not recommended"); + dispatch_sync(dispatch_get_main_queue(), ^{ + image = [self UIImage:json]; + }); + return image; + } + + NSURL *URL = imageSource.imageURL; + NSString *scheme = URL.scheme.lowercaseString; + if ([scheme isEqualToString:@"file"]) { + NSString *assetName = RCTBundlePathForURL(URL); + image = [UIImage imageNamed:assetName]; + if (!image) { + // Attempt to load from the file system + NSString *filePath = URL.path; + if (filePath.pathExtension.length == 0) { + filePath = [filePath stringByAppendingPathExtension:@"png"]; + } + image = [UIImage imageWithContentsOfFile:filePath]; + } + } else if ([scheme isEqualToString:@"data"]) { + image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; + } else if ([scheme isEqualToString:@"http"] && imageSource.packagerAsset) { + image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; + } else { + RCTLogConvertError(json, @"an image. Only local files or data URIs are supported"); + } + + CGFloat scale = imageSource.scale; + if (!scale && imageSource.size.width) { + // If no scale provided, set scale to image width / source width + scale = CGImageGetWidth(image.CGImage) / imageSource.size.width; + } + + if (scale) { + image = [UIImage imageWithCGImage:image.CGImage + scale:scale + orientation:image.imageOrientation]; + } + + if (!CGSizeEqualToSize(imageSource.size, CGSizeZero) && + !CGSizeEqualToSize(imageSource.size, image.size)) { + RCTLogError(@"Image source size %@ does not match loaded image size %@.", + NSStringFromCGSize(imageSource.size), NSStringFromCGSize(image.size)); + } + + return image; +} + ++ (CGImageRef)CGImage:(id)json +{ + return [self UIImage:json].CGImage; +} + +@end diff --git a/React/Base/RCTImageSource.h b/React/Base/RCTImageSource.h new file mode 100644 index 000000000..efcefaa2b --- /dev/null +++ b/React/Base/RCTImageSource.h @@ -0,0 +1,43 @@ +/** + * 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 "RCTConvert.h" + +/** + * Object containing an image URL and associated metadata. + */ +@interface RCTImageSource : NSObject + +@property (nonatomic, strong, readonly) NSURL *imageURL; +@property (nonatomic, assign, readonly) CGSize size; +@property (nonatomic, assign, readonly) CGFloat scale; + +/** + * Create a new image source object. + * Pass a size of CGSizeZero if you do not know or wish to specify the image + * size. Pass a scale of zero if you do not know or wish to specify the scale. + */ +- (instancetype)initWithURL:(NSURL *)url + size:(CGSize)size + scale:(CGFloat)scale; + +/** + * Create a copy of the image source with the specified size and scale. + */ +- (instancetype)imageSourceWithSize:(CGSize)size scale:(CGFloat)scale; + +@end + +@interface RCTConvert (ImageSource) + ++ (RCTImageSource *)RCTImageSource:(id)json; + +@end diff --git a/React/Base/RCTImageSource.m b/React/Base/RCTImageSource.m new file mode 100644 index 000000000..3392385f8 --- /dev/null +++ b/React/Base/RCTImageSource.m @@ -0,0 +1,83 @@ +/** + * 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 "RCTImageSource.h" + +@interface RCTImageSource () + +@property (nonatomic, assign) BOOL packagerAsset; + +@end + +@implementation RCTImageSource + +- (instancetype)initWithURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale +{ + if ((self = [super init])) { + _imageURL = url; + _size = size; + _scale = scale; + } + return self; +} + +- (instancetype)imageSourceWithSize:(CGSize)size scale:(CGFloat)scale +{ + RCTImageSource *imageSource = [[RCTImageSource alloc] initWithURL:_imageURL + size:size + scale:scale]; + imageSource.packagerAsset = _packagerAsset; + return imageSource; +} + +- (BOOL)isEqual:(RCTImageSource *)object +{ + if (![object isKindOfClass:[RCTImageSource class]]) { + return NO; + } + return [_imageURL isEqual:object.imageURL] && _scale == object.scale && + (CGSizeEqualToSize(_size, object.size) || CGSizeEqualToSize(object.size, CGSizeZero)); +} + +@end + +@implementation RCTConvert (ImageSource) + ++ (RCTImageSource *)RCTImageSource:(id)json +{ + if (!json) { + return nil; + } + + NSURL *imageURL; + CGSize size = CGSizeZero; + CGFloat scale = 1.0; + BOOL packagerAsset = NO; + if ([json isKindOfClass:[NSDictionary class]]) { + if (!(imageURL = [self NSURL:json[@"uri"]])) { + return nil; + } + size = [self CGSize:json]; + scale = [self CGFloat:json[@"scale"]] ?: [self BOOL:json[@"deprecated"]] ? 0.0 : 1.0; + packagerAsset = [self BOOL:json[@"__packager_asset"]]; + } else if ([json isKindOfClass:[NSString class]]) { + imageURL = [self NSURL:json]; + } else { + RCTLogConvertError(json, @"an image. Did you forget to call resolveAssetSource() on the JS side?"); + return nil; + } + + RCTImageSource *imageSource = [[RCTImageSource alloc] initWithURL:imageURL + size:size + scale:scale]; + imageSource.packagerAsset = packagerAsset; + return imageSource; +} + +@end diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index 86392db1f..f09aff78b 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -206,6 +206,7 @@ void _RCTLogNativeInternal(RCTLogLevel level, const char *fileName, int lineNumb } #if RCT_DEBUG + // Log to red box in debug mode. if ([UIApplication sharedApplication] && level >= RCTLOG_REDBOX_LEVEL) { NSArray *stackSymbols = [NSThread callStackSymbols]; @@ -233,7 +234,9 @@ void _RCTLogNativeInternal(RCTLogLevel level, const char *fileName, int lineNumb // Log to JS executor [[RCTBridge currentBridge] logMessage:message level:level ? @(RCTLogLevels[level]) : @"info"]; + #endif + } } diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 6d671d58b..53a11ed1d 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080131A69489C00A75B9A /* RCTNavItemManager.m */; }; 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */; }; 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; }; + 13BB3D021BECD54500932C10 /* RCTImageSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BB3D011BECD54500932C10 /* RCTImageSource.m */; }; 13B202011BFB945300C07393 /* RCTPasteboard.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B202001BFB945300C07393 /* RCTPasteboard.m */; }; 13B202041BFB948C00C07393 /* RCTMapAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B202031BFB948C00C07393 /* RCTMapAnnotation.m */; }; 13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; }; @@ -173,7 +174,9 @@ 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorViewManager.m; sourceTree = ""; }; 13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = ""; }; 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; }; - 13B201FF1BFB945300C07393 /* RCTPasteboard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPasteboard.h; sourceTree = ""; }; + 13BB3D001BECD54500932C10 /* RCTImageSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageSource.h; sourceTree = ""; }; + 13BB3D011BECD54500932C10 /* RCTImageSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageSource.m; sourceTree = ""; }; + 13B201FF1BFB945300C07393 /* RCTPasteboard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPasteboard.h; sourceTree = ""; }; 13B202001BFB945300C07393 /* RCTPasteboard.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPasteboard.m; sourceTree = ""; }; 13B202021BFB948C00C07393 /* RCTMapAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapAnnotation.h; sourceTree = ""; }; 13B202031BFB948C00C07393 /* RCTMapAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapAnnotation.m; sourceTree = ""; }; @@ -526,6 +529,8 @@ 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */, 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */, 83CBBA501A601E3B00E9B192 /* RCTUtils.m */, + 13BB3D001BECD54500932C10 /* RCTImageSource.h */, + 13BB3D011BECD54500932C10 /* RCTImageSource.m */, ); path = Base; sourceTree = ""; @@ -645,6 +650,7 @@ 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */, 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */, 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, + 13BB3D021BECD54500932C10 /* RCTImageSource.m in Sources */, 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */, 1450FF8A1BCFF28A00208362 /* RCTProfileTrampoline-x86_64.S in Sources */, 14F7A0EC1BDA3B3C003C6C10 /* RCTPerfMonitor.m in Sources */, diff --git a/React/Views/RCTNavItem.h b/React/Views/RCTNavItem.h index 418d5c5c9..fdfeb66e4 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -31,7 +31,7 @@ @property (nonatomic, readonly) UIBarButtonItem *leftButtonItem; @property (nonatomic, readonly) UIBarButtonItem *rightButtonItem; -@property (nonatomic, copy) RCTBubblingEventBlock onNavLeftButtonTap; -@property (nonatomic, copy) RCTBubblingEventBlock onNavRightButtonTap; +@property (nonatomic, copy) RCTBubblingEventBlock onLeftButtonPress; +@property (nonatomic, copy) RCTBubblingEventBlock onRightButtonPress; @end diff --git a/React/Views/RCTNavItem.m b/React/Views/RCTNavItem.m index 5e4043f84..5fa8005c9 100644 --- a/React/Views/RCTNavItem.m +++ b/React/Views/RCTNavItem.m @@ -67,14 +67,14 @@ [[UIBarButtonItem alloc] initWithImage:_leftButtonIcon style:UIBarButtonItemStylePlain target:self - action:@selector(handleNavLeftButtonTapped)]; + action:@selector(handleLeftButtonPress)]; } else if (_leftButtonTitle.length) { _leftButtonItem = [[UIBarButtonItem alloc] initWithTitle:_leftButtonTitle style:UIBarButtonItemStylePlain target:self - action:@selector(handleNavLeftButtonTapped)]; + action:@selector(handleLeftButtonPress)]; } else { _leftButtonItem = nil; } @@ -82,10 +82,10 @@ return _leftButtonItem; } -- (void)handleNavLeftButtonTapped +- (void)handleLeftButtonPress { - if (_onNavLeftButtonTap) { - _onNavLeftButtonTap(nil); + if (_onLeftButtonPress) { + _onLeftButtonPress(nil); } } @@ -109,14 +109,14 @@ [[UIBarButtonItem alloc] initWithImage:_rightButtonIcon style:UIBarButtonItemStylePlain target:self - action:@selector(handleNavRightButtonTapped)]; + action:@selector(handleRightButtonPress)]; } else if (_rightButtonTitle.length) { _rightButtonItem = [[UIBarButtonItem alloc] initWithTitle:_rightButtonTitle style:UIBarButtonItemStylePlain target:self - action:@selector(handleNavRightButtonTapped)]; + action:@selector(handleRightButtonPress)]; } else { _rightButtonItem = nil; } @@ -124,10 +124,10 @@ return _rightButtonItem; } -- (void)handleNavRightButtonTapped +- (void)handleRightButtonPress { - if (_onNavRightButtonTap) { - _onNavRightButtonTap(nil); + if (_onRightButtonPress) { + _onRightButtonPress(nil); } } diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index 040b7ffce..be4f879dd 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -39,7 +39,7 @@ RCT_EXPORT_VIEW_PROPERTY(leftButtonIcon, UIImage) RCT_EXPORT_VIEW_PROPERTY(rightButtonIcon, UIImage) RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString) -RCT_EXPORT_VIEW_PROPERTY(onNavLeftButtonTap, RCTBubblingEventBlock) -RCT_EXPORT_VIEW_PROPERTY(onNavRightButtonTap, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onLeftButtonPress, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onRightButtonPress, RCTBubblingEventBlock) @end diff --git a/React/Views/RCTTabBarItem.h b/React/Views/RCTTabBarItem.h index def1abf6c..cee2df982 100644 --- a/React/Views/RCTTabBarItem.h +++ b/React/Views/RCTTabBarItem.h @@ -13,7 +13,9 @@ @interface RCTTabBarItem : UIView -@property (nonatomic, copy) id icon; +@property (nonatomic, copy) id /* NSString or NSNumber */ badge; +@property (nonatomic, strong) UIImage *icon; +@property (nonatomic, assign) UITabBarSystemItem systemIcon; @property (nonatomic, assign, getter=isSelected) BOOL selected; @property (nonatomic, readonly) UITabBarItem *barItem; @property (nonatomic, copy) RCTBubblingEventBlock onPress; diff --git a/React/Views/RCTTabBarItem.m b/React/Views/RCTTabBarItem.m index 8699ff96d..a05ae75ee 100644 --- a/React/Views/RCTTabBarItem.m +++ b/React/Views/RCTTabBarItem.m @@ -13,73 +13,78 @@ #import "RCTLog.h" #import "UIView+React.h" +@implementation RCTConvert (UITabBarSystemItem) + +RCT_ENUM_CONVERTER(UITabBarSystemItem, (@{ + @"bookmarks": @(UITabBarSystemItemBookmarks), + @"contacts": @(UITabBarSystemItemContacts), + @"downloads": @(UITabBarSystemItemDownloads), + @"favorites": @(UITabBarSystemItemFavorites), + @"featured": @(UITabBarSystemItemFeatured), + @"history": @(UITabBarSystemItemHistory), + @"more": @(UITabBarSystemItemMore), + @"most-recent": @(UITabBarSystemItemMostRecent), + @"most-viewed": @(UITabBarSystemItemMostViewed), + @"recents": @(UITabBarSystemItemRecents), + @"search": @(UITabBarSystemItemSearch), + @"top-rated": @(UITabBarSystemItemTopRated), +}), NSNotFound, integerValue) + +@end + @implementation RCTTabBarItem @synthesize barItem = _barItem; +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + _systemIcon = NSNotFound; + } + return self; +} + - (UITabBarItem *)barItem { if (!_barItem) { _barItem = [UITabBarItem new]; + _systemIcon = NSNotFound; } return _barItem; } -- (void)setIcon:(id)icon +- (void)setBadge:(id)badge { - static NSDictionary *systemIcons; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - systemIcons = @{ - @"bookmarks": @(UITabBarSystemItemBookmarks), - @"contacts": @(UITabBarSystemItemContacts), - @"downloads": @(UITabBarSystemItemDownloads), - @"favorites": @(UITabBarSystemItemFavorites), - @"featured": @(UITabBarSystemItemFeatured), - @"history": @(UITabBarSystemItemHistory), - @"more": @(UITabBarSystemItemMore), - @"most-recent": @(UITabBarSystemItemMostRecent), - @"most-viewed": @(UITabBarSystemItemMostViewed), - @"recents": @(UITabBarSystemItemRecents), - @"search": @(UITabBarSystemItemSearch), - @"top-rated": @(UITabBarSystemItemTopRated), - }; - }); + _badge = [badge copy]; + self.barItem.badgeValue = [badge description]; +} - // Update icon - BOOL wasSystemIcon = (systemIcons[_icon] != nil); - _icon = [icon copy]; - - // Check if string matches any custom images first - UIImage *image = [RCTConvert UIImage:_icon]; - UITabBarItem *oldItem = _barItem; - if (image) { - // Recreate barItem if previous item was a system icon. Calling self.barItem - // creates a new instance if it wasn't set yet. - if (wasSystemIcon) { - _barItem = nil; - self.barItem.image = image; - } else { - self.barItem.image = image; - return; - } - } else if ([icon isKindOfClass:[NSString class]] && [icon length] > 0) { - // Not a custom image, may be a system item? - NSNumber *systemIcon = systemIcons[icon]; - if (!systemIcon) { - RCTLogError(@"The tab bar icon '%@' did not match any known image or system icon", icon); - return; - } - _barItem = [[UITabBarItem alloc] initWithTabBarSystemItem:systemIcon.integerValue tag:oldItem.tag]; - } else { - self.barItem.image = nil; +- (void)setSystemIcon:(UITabBarSystemItem)systemIcon +{ + if (_systemIcon != systemIcon) { + _systemIcon = systemIcon; + UITabBarItem *oldItem = _barItem; + _barItem = [[UITabBarItem alloc] initWithTabBarSystemItem:_systemIcon + tag:oldItem.tag]; + _barItem.title = oldItem.title; + _barItem.imageInsets = oldItem.imageInsets; + _barItem.badgeValue = oldItem.badgeValue; } +} - // Reapply previous properties - _barItem.title = oldItem.title; - _barItem.imageInsets = oldItem.imageInsets; - _barItem.selectedImage = oldItem.selectedImage; - _barItem.badgeValue = oldItem.badgeValue; +- (void)setIcon:(UIImage *)icon +{ + _icon = icon; + if (_icon && _systemIcon != NSNotFound) { + _systemIcon = NSNotFound; + UITabBarItem *oldItem = _barItem; + _barItem = [UITabBarItem new]; + _barItem.title = oldItem.title; + _barItem.imageInsets = oldItem.imageInsets; + _barItem.selectedImage = oldItem.selectedImage; + _barItem.badgeValue = oldItem.badgeValue; + } + self.barItem.image = _icon; } - (UIViewController *)reactViewController diff --git a/React/Views/RCTTabBarItemManager.m b/React/Views/RCTTabBarItemManager.m index a926a54f3..d92e3a2d9 100644 --- a/React/Views/RCTTabBarItemManager.m +++ b/React/Views/RCTTabBarItemManager.m @@ -21,10 +21,11 @@ RCT_EXPORT_MODULE() return [RCTTabBarItem new]; } +RCT_EXPORT_VIEW_PROPERTY(badge, id /* NSString or NSNumber */) RCT_EXPORT_VIEW_PROPERTY(selected, BOOL) -RCT_EXPORT_VIEW_PROPERTY(icon, id) +RCT_EXPORT_VIEW_PROPERTY(icon, UIImage) RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage, UIImage) -RCT_REMAP_VIEW_PROPERTY(badge, barItem.badgeValue, NSString) +RCT_EXPORT_VIEW_PROPERTY(systemIcon, UITabBarSystemItem) RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem) {