From 8e70c7f00320db6873ea1da51c21f46a23a2ce60 Mon Sep 17 00:00:00 2001 From: Dmitriy Loktev Date: Thu, 9 Jul 2015 15:48:22 -0100 Subject: [PATCH] [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 ? {this.state.progress}% : null; return 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} } } ``` 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 `` (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 --- Libraries/Image/Image.ios.js | 30 +++++++- Libraries/Image/RCTDownloadTaskWrapper.h | 26 +++++++ Libraries/Image/RCTDownloadTaskWrapper.m | 69 +++++++++++++++++++ .../Image/RCTImage.xcodeproj/project.pbxproj | 6 ++ Libraries/Image/RCTImageDownloader.h | 4 ++ Libraries/Image/RCTImageDownloader.m | 24 ++++--- Libraries/Image/RCTImageLoader.m | 2 +- Libraries/Image/RCTNetworkImageView.h | 4 +- Libraries/Image/RCTNetworkImageView.m | 53 ++++++++++++-- Libraries/Image/RCTNetworkImageViewManager.m | 23 +++++-- 10 files changed, 217 insertions(+), 24 deletions(-) create mode 100644 Libraries/Image/RCTDownloadTaskWrapper.h create mode 100644 Libraries/Image/RCTDownloadTaskWrapper.m diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 7a629ce9a6..63534af3e6 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -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 ; } }); @@ -178,6 +205,7 @@ var nativeOnlyProps = { src: true, defaultImageSrc: true, imageTag: true, + progressHandlerRegistered: true }; if (__DEV__) { verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps); diff --git a/Libraries/Image/RCTDownloadTaskWrapper.h b/Libraries/Image/RCTDownloadTaskWrapper.h new file mode 100644 index 0000000000..e1660218cd --- /dev/null +++ b/Libraries/Image/RCTDownloadTaskWrapper.h @@ -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 + +typedef void (^RCTDataCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error); +typedef void (^RCTDataProgressBlock)(int64_t written, int64_t total); + +@interface RCTDownloadTaskWrapper : NSObject + +@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 delegate; + +@end diff --git a/Libraries/Image/RCTDownloadTaskWrapper.m b/Libraries/Image/RCTDownloadTaskWrapper.m new file mode 100644 index 0000000000..0d5f467e43 --- /dev/null +++ b/Libraries/Image/RCTDownloadTaskWrapper.m @@ -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 diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 1735f33b4c..0127cfd8e6 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -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 = ""; }; + 03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTaskWrapper.m; sourceTree = ""; }; 1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImage.h; sourceTree = ""; }; 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImage.m; sourceTree = ""; }; 1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImageManager.h; sourceTree = ""; }; @@ -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; diff --git a/Libraries/Image/RCTImageDownloader.h b/Libraries/Image/RCTImageDownloader.h index 5a4dd1987a..1829138300 100644 --- a/Libraries/Image/RCTImageDownloader.h +++ b/Libraries/Image/RCTImageDownloader.h @@ -9,6 +9,8 @@ #import +#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; /** diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index ea448220b7..e45c41242b 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -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; diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 04fa17f5d4..0e4a9c171c 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -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 { diff --git a/Libraries/Image/RCTNetworkImageView.h b/Libraries/Image/RCTNetworkImageView.h index e04f71e328..6cdf31216a 100644 --- a/Libraries/Image/RCTNetworkImageView.h +++ b/Libraries/Image/RCTNetworkImageView.h @@ -9,11 +9,13 @@ #import +@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, diff --git a/Libraries/Image/RCTNetworkImageView.m b/Libraries/Image/RCTNetworkImageView.m index 5a286e0202..8c6748f764 100644 --- a/Libraries/Image/RCTNetworkImageView.m +++ b/Libraries/Image/RCTNetworkImageView.m @@ -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]); } }]; } diff --git a/Libraries/Image/RCTNetworkImageViewManager.m b/Libraries/Image/RCTNetworkImageViewManager.m index 3fcd4a75be..a44d198c12 100644 --- a/Libraries/Image/RCTNetworkImageViewManager.m +++ b/Libraries/Image/RCTNetworkImageViewManager.m @@ -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