Added RCTImageSource

Summary:
public

The +[RCTConvert UIImage:] function, while convenient, is inherently limited by being synchronous, which means that it cannot be used to load remote images, and may not be efficient for local images either. It's also unable to access the bridge, which means that it cannot take advantage of the modular image-loading pipeline.

This diff introduces a new RCTImageSource class which can be used to pass image source objects over the bridge and defer loading until later.

I've also added automatic application of the `resolveAssetSource()` function based on prop type, and fixed up the image logic in NavigatorIOS and TabBarIOS.

Reviewed By: javache

Differential Revision: D2631541

fb-gh-sync-id: 6604635e8bb5394425102487f1ee7cd729321877
This commit is contained in:
Nick Lockwood 2015-12-08 03:29:08 -08:00 committed by facebook-github-bot-4
parent dcebe8cd37
commit b672294858
23 changed files with 434 additions and 276 deletions

View File

@ -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 (
<StaticContainer key={'nav' + i} shouldUpdate={shouldUpdateChild}>
<RCTNavigatorItem
title={route.title}
{...route}
{...props}
style={[
styles.stackItem,
this.props.itemWrapperStyle,
route.wrapperStyle
]}
backButtonIcon={resolveAssetSource(route.backButtonIcon)}
backButtonTitle={route.backButtonTitle}
leftButtonIcon={resolveAssetSource(route.leftButtonIcon)}
leftButtonTitle={route.leftButtonTitle}
onNavLeftButtonTap={route.onLeftButtonPress}
rightButtonIcon={resolveAssetSource(route.rightButtonIcon)}
rightButtonTitle={route.rightButtonTitle}
onNavRightButtonTap={route.onRightButtonPress}
navigationBarHidden={this.props.navigationBarHidden}
shadowHidden={this.props.shadowHidden}
tintColor={this.props.tintColor}
barTintColor={this.props.barTintColor}
translucent={this.props.translucent !== false}
titleTextColor={this.props.titleTextColor}>
itemWrapperStyle,
wrapperStyle
]}>
<Component
navigator={this.navigator}
route={route}
{...route.passProps}
{...passProps}
/>
</RCTNavigatorItem>
</StaticContainer>

View File

@ -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 =
<StaticContainer shouldUpdate={this.props.selected}>
{this.props.children}
{children}
</StaticContainer>;
} else {
tabContents = <View />;
var tabContents = <View />;
}
var badge = typeof this.props.badge === 'number' ?
'' + this.props.badge :
this.props.badge;
return (
<RCTTabBarItem
{...this.props}
icon={this.props.systemIcon || resolveAssetSource(this.props.icon)}
selectedIcon={resolveAssetSource(this.props.selectedIcon)}
badge={badge}
style={[styles.tab, this.props.style]}>
{...props}
style={[styles.tab, style]}>
{tabContents}
</RCTTabBarItem>
);

View File

@ -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 <View {...this.props} style={style} />;
}
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;

View File

@ -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;

View File

@ -11,6 +11,7 @@
#import "RCTImageComponent.h"
@class RCTBridge;
@class RCTImageSource;
@interface RCTImageView : UIImageView <RCTImageComponent>
@ -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

View File

@ -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.

View File

@ -12,6 +12,7 @@
#import <UIKit/UIKit.h>
#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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
};

View File

@ -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.
*/

View File

@ -12,6 +12,7 @@
#import <objc/message.h>
#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

View File

@ -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 <Foundation/Foundation.h>
#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

View File

@ -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

View File

@ -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<NSString *> *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
}
}

View File

@ -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 = "<group>"; };
13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = "<group>"; };
13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = "<group>"; };
13B201FF1BFB945300C07393 /* RCTPasteboard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPasteboard.h; sourceTree = "<group>"; };
13BB3D001BECD54500932C10 /* RCTImageSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageSource.h; sourceTree = "<group>"; };
13BB3D011BECD54500932C10 /* RCTImageSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageSource.m; sourceTree = "<group>"; };
13B201FF1BFB945300C07393 /* RCTPasteboard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPasteboard.h; sourceTree = "<group>"; };
13B202001BFB945300C07393 /* RCTPasteboard.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPasteboard.m; sourceTree = "<group>"; };
13B202021BFB948C00C07393 /* RCTMapAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapAnnotation.h; sourceTree = "<group>"; };
13B202031BFB948C00C07393 /* RCTMapAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapAnnotation.m; sourceTree = "<group>"; };
@ -526,6 +529,8 @@
1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */,
83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */,
83CBBA501A601E3B00E9B192 /* RCTUtils.m */,
13BB3D001BECD54500932C10 /* RCTImageSource.h */,
13BB3D011BECD54500932C10 /* RCTImageSource.m */,
);
path = Base;
sourceTree = "<group>";
@ -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 */,

View File

@ -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

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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)
{