diff --git a/Examples/UIExplorer/UIExplorer/AppDelegate.m b/Examples/UIExplorer/UIExplorer/AppDelegate.m index d56ac42a7..75a24b172 100644 --- a/Examples/UIExplorer/UIExplorer/AppDelegate.m +++ b/Examples/UIExplorer/UIExplorer/AppDelegate.m @@ -73,9 +73,11 @@ } - (void)loadSourceForBridge:(RCTBridge *)bridge - withBlock:(RCTSourceLoadBlock)loadCallback + onProgress:(RCTSourceLoadProgressBlock)onProgress + onComplete:(RCTSourceLoadBlock)loadCallback { [RCTJavaScriptLoader loadBundleAtURL:[self sourceURLForBridge:bridge] + onProgress:onProgress onComplete:loadCallback]; } diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 5e3f2572b..80df61ff7 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -24,6 +24,7 @@ #import "RCTSourceCode.h" #import "RCTUtils.h" #import "RCTRedBox.h" +#import "RCTDevLoadingView.h" #define RCTAssertJSThread() \ RCTAssert(![NSStringFromClass([self->_javaScriptExecutor class]) isEqualToString:@"RCTJSCExecutor"] || \ @@ -115,6 +116,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele sourceCode = source; dispatch_group_leave(initModulesAndLoadSource); + } onProgress:^(RCTLoadingProgress *progressData) { +#ifdef RCT_DEV + RCTDevLoadingView *loadingView = [weakSelf moduleForClass:[RCTDevLoadingView class]]; + [loadingView updateProgress:progressData]; +#endif }]; // Synchronously initialize all native modules that cannot be loaded lazily @@ -172,7 +178,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); } -- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad +- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress { [_performanceLogger markStartForTag:RCTPLScriptDownload]; @@ -183,18 +189,20 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele _onSourceLoad(error, source, sourceLength); }; - if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) { + if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:onProgress:onComplete:)]) { + [self.delegate loadSourceForBridge:_parentBridge onProgress:onProgress onComplete:onSourceLoad]; + } else if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) { [self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad]; } else { RCTAssert(self.bundleURL, @"bundleURL must be non-nil when not implementing loadSourceForBridge"); - [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:^(NSError *error, NSData *source, int64_t sourceLength) { + [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:^(NSError *error, NSData *source, int64_t sourceLength) { if (error && [self.delegate respondsToSelector:@selector(fallbackSourceURLForBridge:)]) { NSURL *fallbackURL = [self.delegate fallbackSourceURLForBridge:self->_parentBridge]; if (fallbackURL && ![fallbackURL isEqual:self.bundleURL]) { RCTLogError(@"Failed to load bundle(%@) with error:(%@)", self.bundleURL, error.localizedDescription); self.bundleURL = fallbackURL; - [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:onSourceLoad]; + [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:onSourceLoad]; return; } } diff --git a/React/Base/RCTBridgeDelegate.h b/React/Base/RCTBridgeDelegate.h index f866c94cf..c5f0cdb40 100644 --- a/React/Base/RCTBridgeDelegate.h +++ b/React/Base/RCTBridgeDelegate.h @@ -87,6 +87,14 @@ * location specified by the `sourceURLForBridge:` method, however, if you want * to handle loading the JS yourself, you can do so by implementing this method. */ +- (void)loadSourceForBridge:(RCTBridge *)bridge + onProgress:(RCTSourceLoadProgressBlock)onProgress + onComplete:(RCTSourceLoadBlock)loadCallback; + +/** + * Similar to loadSourceForBridge:onProgress:onComplete: but without progress + * reporting. + */ - (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)loadCallback; diff --git a/React/Base/RCTJavaScriptLoader.h b/React/Base/RCTJavaScriptLoader.h index 141649556..4c5b962ad 100755 --- a/React/Base/RCTJavaScriptLoader.h +++ b/React/Base/RCTJavaScriptLoader.h @@ -23,11 +23,20 @@ NS_ENUM(NSInteger) { RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously = 1000, }; +@interface RCTLoadingProgress : NSObject + +@property (nonatomic, copy) NSString *status; +@property (strong, nonatomic) NSNumber *done; +@property (strong, nonatomic) NSNumber *total; + +@end + +typedef void (^RCTSourceLoadProgressBlock)(RCTLoadingProgress *progressData); typedef void (^RCTSourceLoadBlock)(NSError *error, NSData *source, int64_t sourceLength); @interface RCTJavaScriptLoader : NSObject -+ (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete; ++ (void)loadBundleAtURL:(NSURL *)scriptURL onProgress:(RCTSourceLoadProgressBlock)onProgress onComplete:(RCTSourceLoadBlock)onComplete; /** * @experimental diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index cb19dce80..58988920a 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -22,11 +22,27 @@ uint32_t const RCTRAMBundleMagicNumber = 0xFB0BD1E5; NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain"; +@implementation RCTLoadingProgress + +- (NSString *)description +{ + NSMutableString *desc = [NSMutableString new]; + [desc appendString:_status ?: @"Loading"]; + + if (_total > 0) { + [desc appendFormat:@" %ld%% (%@/%@)", (long)(100 * [_done integerValue] / [_total integerValue]), _done, _total]; + } + [desc appendString:@"…"]; + return desc; +} + +@end + @implementation RCTJavaScriptLoader RCT_NOT_IMPLEMENTED(- (instancetype)init) -+ (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete ++ (void)loadBundleAtURL:(NSURL *)scriptURL onProgress:(RCTSourceLoadProgressBlock)onProgress onComplete:(RCTSourceLoadBlock)onComplete { int64_t sourceLength; NSError *error; @@ -43,7 +59,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) && error.code == RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously; if (isCannotLoadSyncError) { - attemptAsynchronousLoadOfBundleAtURL(scriptURL, onComplete); + attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete); } else { onComplete(error, nil, 0); } @@ -136,7 +152,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) return [NSData dataWithBytes:&magicNumber length:sizeof(magicNumber)]; } -static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoadBlock onComplete) +static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoadProgressBlock onProgress, RCTSourceLoadBlock onComplete) { scriptURL = sanitizeURL(scriptURL); @@ -155,7 +171,9 @@ static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoad RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) { if (!done) { - // TODO(frantic): Emit progress event + if (onProgress) { + onProgress(progressEventFromData(data)); + } return; } @@ -206,6 +224,21 @@ static NSURL *sanitizeURL(NSURL *url) return [RCTConvert NSURL:url.absoluteString]; } +static RCTLoadingProgress *progressEventFromData(NSData *rawData) +{ + NSString *text = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding]; + id info = RCTJSONParse(text, nil); + if (!info || ![info isKindOfClass:[NSDictionary class]]) { + return nil; + } + + RCTLoadingProgress *progress = [RCTLoadingProgress new]; + progress.status = [info valueForKey:@"status"]; + progress.done = [info valueForKey:@"done"]; + progress.total = [info valueForKey:@"total"]; + return progress; +} + static NSDictionary *userInfoForRawResponse(NSString *rawText) { NSDictionary *parsedResponse = RCTJSONParse(rawText, nil); diff --git a/React/Modules/RCTDevLoadingView.h b/React/Modules/RCTDevLoadingView.h index b09315fb6..2d9f15c4a 100644 --- a/React/Modules/RCTDevLoadingView.h +++ b/React/Modules/RCTDevLoadingView.h @@ -12,5 +12,6 @@ @interface RCTDevLoadingView : NSObject + (void)setEnabled:(BOOL)enabled; +- (void)updateProgress:(RCTLoadingProgress *)progress; @end diff --git a/React/Modules/RCTDevLoadingView.m b/React/Modules/RCTDevLoadingView.m index 0b7ee4996..89f645ff8 100644 --- a/React/Modules/RCTDevLoadingView.m +++ b/React/Modules/RCTDevLoadingView.m @@ -142,6 +142,16 @@ RCT_EXPORT_METHOD(hide) backgroundColor:backgroundColor]; } +- (void)updateProgress:(RCTLoadingProgress *)progress +{ + if (!progress) { + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ + self->_label.text = [progress description]; + }); +} + @end #else @@ -150,6 +160,7 @@ RCT_EXPORT_METHOD(hide) + (NSString *)moduleName { return nil; } + (void)setEnabled:(BOOL)enabled { } +- (void)updateProgress:(RCTLoadingProgress *)progress {} @end