[Image] Add onLoadStart, onLoadProgress, onLoadError events to Image component

Summary:
This PR adds 4 native events to NetworkImage.

![demo](http://zippy.gfycat.com/MelodicLawfulCaecilian.gif)

Using these events I could wrap `Image` component into something like:
```javascript
class NetworkImage extends React.Component {
  getInitialState() {
    return {
      downloading: false,
      progress: 0
    }
  }

  render() {
    var loader = this.state.downloading ?
      <View style={this.props.loaderStyles}>
        <ActivityIndicatorIOS animating={true} size={'large'} />
        <Text style={{color: '#bbb'}}>{this.state.progress}%</Text>
      </View>
      :
      null;

    return <Image source={this.props.source}
      onLoadStart={() => this.setState({downloading: true}) }
      onLoaded={() => this.setState({downloading: false}) }
      onLoadProgress={(e)=> this.setState({progress: Math.round(100 * e.nativeEvent.written / e.nativeEvent.total)});
      onLoadError={(e)=> {
        alert('the image cannot be downloaded because: ', JSON.stringify(e));
        this.setState({downloading: false});
      }}>
      {loader}
    </Image>
  }
}
```
Useful on slow connections and server errors.

There are dozen lines of Objective C, which I don't have experience with. There are neither specific tests nor documentation yet. And I do realize that you're already working right now on better `<Image/>` (pipeline, new asset management, etc.). So this is basically a proof concept of events for images, and if this idea is not completely wrong I could improve it or help somehow.

Closes https://github.com/facebook/react-native/pull/1318
Github Author: Dmitriy Loktev <unknownliveid@hotmail.com>
This commit is contained in:
Dmitriy Loktev 2015-07-09 15:48:22 -01:00
parent 54c21ac651
commit 8e70c7f003
10 changed files with 217 additions and 24 deletions

View File

@ -104,7 +104,33 @@ var Image = React.createClass({
*
* {nativeEvent: { layout: {x, y, width, height}}}.
*/
onLayout: PropTypes.func,
onLayout: PropTypes.func,
/**
* Invoked on load start
*/
onLoadStart: PropTypes.func,
/**
* Invoked on download progress with
*
* {nativeEvent: { written, total}}.
*/
onLoadProgress: PropTypes.func,
/**
* Invoked on load abort
*/
onLoadAbort: PropTypes.func,
/**
* Invoked on load error
*
* {nativeEvent: { error}}.
*/
onLoadError: PropTypes.func,
/**
* Invoked on load end
*
*/
onLoaded: PropTypes.func
},
statics: {
@ -161,6 +187,7 @@ var Image = React.createClass({
if (this.props.defaultSource) {
nativeProps.defaultImageSrc = this.props.defaultSource.uri;
}
nativeProps.progressHandlerRegistered = isNetwork && this.props.onLoadProgress;
return <RawImage {...nativeProps} />;
}
});
@ -178,6 +205,7 @@ var nativeOnlyProps = {
src: true,
defaultImageSrc: true,
imageTag: true,
progressHandlerRegistered: true
};
if (__DEV__) {
verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps);

View File

@ -0,0 +1,26 @@
/**
* 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>
typedef void (^RCTDataCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error);
typedef void (^RCTDataProgressBlock)(int64_t written, int64_t total);
@interface RCTDownloadTaskWrapper : NSObject <NSURLSessionDownloadDelegate>
@property (copy, nonatomic) RCTDataCompletionBlock completionBlock;
@property (copy, nonatomic) RCTDataProgressBlock progressBlock;
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration delegateQueue:(NSOperationQueue *)delegateQueue;
- (NSURLSessionDownloadTask *)downloadData:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock completionBlock:(RCTDataCompletionBlock)completionBlock;
@property (nonatomic, assign) id<NSURLSessionDownloadDelegate> delegate;
@end

View File

@ -0,0 +1,69 @@
/**
* 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 "RCTDownloadTaskWrapper.h"
@implementation RCTDownloadTaskWrapper
{
NSURLSession *_URLSession;
}
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration delegateQueue:(NSOperationQueue *)delegateQueue
{
if ((self = [super init])) {
_URLSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
}
return self;
}
- (NSURLSessionDownloadTask *)downloadData:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock completionBlock:(RCTDataCompletionBlock)completionBlock
{
self.completionBlock = completionBlock;
self.progressBlock = progressBlock;
NSURLSessionDownloadTask *task = [_URLSession downloadTaskWithURL:url completionHandler:nil];
[task resume];
return task;
}
#pragma mark - NSURLSessionTaskDelegate methods
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
if (self.completionBlock) {
NSData *data = [NSData dataWithContentsOfURL:location];
dispatch_async(dispatch_get_main_queue(), ^{
self.completionBlock(downloadTask.response, data, nil);
});
}
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)didWriteData totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.progressBlock != nil) {
self.progressBlock(totalBytesWritten, totalBytesExpectedToWrite);
}
});
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
if (error && self.completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
self.completionBlock(NULL, NULL, error);
});
}
}
@end

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */; };
1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */; };
1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; };
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; };
@ -32,6 +33,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
03559E7D1B064D3A00730281 /* RCTDownloadTaskWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTDownloadTaskWrapper.h; sourceTree = "<group>"; };
03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTaskWrapper.m; sourceTree = "<group>"; };
1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImage.h; sourceTree = "<group>"; };
1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImage.m; sourceTree = "<group>"; };
1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImageManager.h; sourceTree = "<group>"; };
@ -71,6 +74,8 @@
children = (
143879331AAD238D00F088A5 /* RCTCameraRollManager.h */,
143879341AAD238D00F088A5 /* RCTCameraRollManager.m */,
03559E7D1B064D3A00730281 /* RCTDownloadTaskWrapper.h */,
03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */,
1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */,
1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */,
58B511891A9E6BD600147676 /* RCTImageDownloader.h */,
@ -168,6 +173,7 @@
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */,
143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */,
143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */,
03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */,
1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -9,6 +9,8 @@
#import <UIKit/UIKit.h>
#import "RCTDownloadTaskWrapper.h"
typedef void (^RCTDataDownloadBlock)(NSData *data, NSError *error);
typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error);
@ -22,6 +24,7 @@ typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error);
* the main thread. Returns a token that can be used to cancel the download.
*/
- (id)downloadDataForURL:(NSURL *)url
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTDataDownloadBlock)block;
/**
@ -35,6 +38,7 @@ typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error);
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
backgroundColor:(UIColor *)backgroundColor
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTImageDownloadBlock)block;
/**

View File

@ -9,6 +9,7 @@
#import "RCTImageDownloader.h"
#import "RCTDownloadTaskWrapper.h"
#import "RCTLog.h"
#import "RCTUtils.h"
@ -45,12 +46,15 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
return self;
}
- (id)_downloadDataForURL:(NSURL *)url block:(RCTCachedDataDownloadBlock)block
- (id)_downloadDataForURL:(NSURL *)url progressBlock:progressBlock block:(RCTCachedDataDownloadBlock)block
{
NSString *cacheKey = url.absoluteString;
__block BOOL cancelled = NO;
__block NSURLSessionDataTask *task = nil;
__block NSURLSessionDownloadTask *task = nil;
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
RCTDownloadTaskWrapper *downloadTaskWrapper = [[RCTDownloadTaskWrapper alloc] initWithSessionConfiguration:config delegateQueue:nil];
dispatch_block_t cancel = ^{
cancelled = YES;
@ -85,7 +89,8 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
});
};
task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
task = [downloadTaskWrapper downloadData:url progressBlock:progressBlock completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!cancelled) {
runBlocks(NO, data, error);
}
@ -93,12 +98,12 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
if (response) {
RCTImageDownloader *strongSelf = weakSelf;
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data userInfo:nil storagePolicy:NSURLCacheStorageAllowed];
[strongSelf->_cache storeCachedResponse:cachedResponse forDataTask:task];
[strongSelf->_cache storeCachedResponse:cachedResponse forRequest:request];
}
task = nil;
}];
[_cache getCachedResponseForDataTask:task completionHandler:^(NSCachedURLResponse *cachedResponse) {
NSCachedURLResponse *cachedResponse = [_cache cachedResponseForRequest:request];
if (cancelled) {
return;
}
@ -108,16 +113,16 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
} else {
[task resume];
}
}];
}
});
return [cancel copy];
}
- (id)downloadDataForURL:(NSURL *)url block:(RCTDataDownloadBlock)block
- (id)downloadDataForURL:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock block:(RCTDataDownloadBlock)block
{
return [self _downloadDataForURL:url block:^(BOOL cached, NSData *data, NSError *error) {
return [self _downloadDataForURL:url progressBlock:progressBlock block:^(BOOL cached, NSData *data, NSError *error) {
block(data, error);
}];
}
@ -127,9 +132,10 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
backgroundColor:(UIColor *)backgroundColor
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTImageDownloadBlock)block
{
return [self downloadDataForURL:url block:^(NSData *data, NSError *error) {
return [self downloadDataForURL:url progressBlock:progressBlock block:^(NSData *data, NSError *error) {
if (!data || error) {
block(nil, error);
return;

View File

@ -121,7 +121,7 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
RCTDispatchCallbackOnMainQueue(callback, RCTErrorWithMessage(errorMessage), nil);
return;
}
[[RCTImageDownloader sharedInstance] downloadDataForURL:url block:^(NSData *data, NSError *error) {
[[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:nil block:^(NSData *data, NSError *error) {
if (error) {
RCTDispatchCallbackOnMainQueue(callback, error, nil);
} else {

View File

@ -9,11 +9,13 @@
#import <UIKit/UIKit.h>
@class RCTEventDispatcher;
@class RCTImageDownloader;
@interface RCTNetworkImageView : UIView
- (instancetype)initWithImageDownloader:(RCTImageDownloader *)imageDownloader NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
imageDownloader:(RCTImageDownloader *)imageDownloader NS_DESIGNATED_INITIALIZER;
/**
* An image that will appear while the view is loading the image from the network,

View File

@ -14,23 +14,27 @@
#import "RCTGIFImage.h"
#import "RCTImageDownloader.h"
#import "RCTUtils.h"
#import "RCTBridgeModule.h"
#import "RCTEventDispatcher.h"
#import "UIView+React.h"
@implementation RCTNetworkImageView
{
BOOL _deferred;
BOOL _progressHandlerRegistered;
NSURL *_imageURL;
NSURL *_deferredImageURL;
NSUInteger _deferSentinel;
RCTImageDownloader *_imageDownloader;
id _downloadToken;
RCTEventDispatcher *_eventDispatcher;
}
- (instancetype)initWithImageDownloader:(RCTImageDownloader *)imageDownloader
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher imageDownloader:(RCTImageDownloader *)imageDownloader
{
RCTAssertParam(imageDownloader);
if ((self = [super initWithFrame:CGRectZero])) {
_eventDispatcher = eventDispatcher;
_progressHandlerRegistered = NO;
_deferSentinel = 0;
_imageDownloader = imageDownloader;
self.userInteractionEnabled = NO;
@ -56,6 +60,11 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
[self _updateImage];
}
- (void)setProgressHandlerRegistered:(BOOL)progressHandlerRegistered
{
_progressHandlerRegistered = progressHandlerRegistered;
}
- (void)reactSetFrame:(CGRect)frame
{
[super reactSetFrame:frame];
@ -89,8 +98,34 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
self.layer.minificationFilter = kCAFilterTrilinear;
self.layer.magnificationFilter = kCAFilterTrilinear;
}
[_eventDispatcher sendInputEventWithName:@"loadStart" body:@{ @"target": self.reactTag }];
RCTDataProgressBlock progressHandler = ^(int64_t written, int64_t total) {
if (_progressHandlerRegistered) {
NSDictionary *event = @{
@"target": self.reactTag,
@"written": @(written),
@"total": @(total),
};
[_eventDispatcher sendInputEventWithName:@"loadProgress" body:event];
}
};
void (^errorHandler)(NSString *errorDescription) = ^(NSString *errorDescription) {
NSDictionary *event = @{
@"target": self.reactTag,
@"error": errorDescription,
};
[_eventDispatcher sendInputEventWithName:@"loadError" body:event];
};
void (^loadEndHandler)(void) = ^(void) {
NSDictionary *event = @{ @"target": self.reactTag };
[_eventDispatcher sendInputEventWithName:@"loaded" body:event];
};
if ([imageURL.pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame) {
_downloadToken = [_imageDownloader downloadDataForURL:imageURL block:^(NSData *data, NSError *error) {
_downloadToken = [_imageDownloader downloadDataForURL:imageURL progressBlock:progressHandler block:^(NSData *data, NSError *error) {
if (data) {
dispatch_async(dispatch_get_main_queue(), ^{
if (imageURL != self.imageURL) {
@ -102,13 +137,16 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
self.layer.minificationFilter = kCAFilterLinear;
self.layer.magnificationFilter = kCAFilterLinear;
[self.layer addAnimation:animation forKey:@"contents"];
loadEndHandler();
});
} else if (error) {
RCTLogWarn(@"Unable to download image data. Error: %@", error);
errorHandler([error description]);
}
}];
} else {
_downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() resizeMode:self.contentMode backgroundColor:self.backgroundColor block:^(UIImage *image, NSError *error) {
_downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale()
resizeMode:self.contentMode backgroundColor:self.backgroundColor
progressBlock:progressHandler block:^(UIImage *image, NSError *error) {
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
if (imageURL != self.imageURL) {
@ -118,9 +156,10 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
[self.layer removeAnimationForKey:@"contents"];
self.layer.contentsScale = image.scale;
self.layer.contents = (__bridge id)image.CGImage;
loadEndHandler();
});
} else if (error) {
RCTLogWarn(@"Unable to download image. Error: %@", error);
errorHandler([error description]);
}
}];
}

View File

@ -9,24 +9,37 @@
#import "RCTNetworkImageViewManager.h"
#import "RCTNetworkImageView.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTUtils.h"
#import "RCTImageDownloader.h"
#import "RCTNetworkImageView.h"
#import "RCTUtils.h"
@implementation RCTNetworkImageViewManager
RCT_EXPORT_MODULE()
@synthesize bridge = _bridge;
- (UIView *)view
{
return [[RCTNetworkImageView alloc] initWithImageDownloader:[RCTImageDownloader sharedInstance]];
return [[RCTNetworkImageView alloc] initWithEventDispatcher:self.bridge.eventDispatcher imageDownloader:[RCTImageDownloader sharedInstance]];
}
RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage)
RCT_REMAP_VIEW_PROPERTY(src, imageURL, NSURL)
RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode)
RCT_EXPORT_VIEW_PROPERTY(progressHandlerRegistered, BOOL)
- (NSDictionary *)customDirectEventTypes
{
return @{
@"loadStart": @{ @"registrationName": @"onLoadStart" },
@"loadProgress": @{ @"registrationName": @"onLoadProgress" },
@"loaded": @{ @"registrationName": @"onLoaded" },
@"loadError": @{ @"registrationName": @"onLoadError" },
@"loadAbort": @{ @"registrationName": @"onLoadAbort" },
};
}
@end