From 03889780b9fc6ee728aa6a6cd5ddd506537bb32a Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 28 May 2015 13:16:06 -0700 Subject: [PATCH] [WIP] Added loadingView property to RCTRootView --- React/Base/RCTRootView.h | 26 ++++++++ React/Base/RCTRootView.m | 89 ++++++++++++++++++++++++---- React/Executors/RCTContextExecutor.m | 12 ++-- 3 files changed, 108 insertions(+), 19 deletions(-) diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index d55094c37..dec81d256 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -11,6 +11,18 @@ #import "RCTBridge.h" +/** + * This notification is sent when the first subviews are added to the root view + * after the application has loaded. This is used to hide the `loadingView`, and + * is a good indicator that the application is ready to use. + */ +extern NSString *const RCTContentDidAppearNotification; + +/** + * Native view used to host React-managed views within the app. Can be used just + * like any ordinary UIView. You can have multiple RCTRootViews on screen at + * once, all controlled by the same JavaScript application. + */ @interface RCTRootView : UIView /** @@ -67,4 +79,18 @@ */ @property (nonatomic, strong, readonly) UIView *contentView; +/** + * A view to display while the JavaScript is loading, so users aren't presented + * with a blank screen. By default this is nil, but you can override it with + * (for example) a UIActivityIndicatorView or a placeholder image. + */ +@property (nonatomic, strong) UIView *loadingView; + +/** + * Timings for hiding the loading view after the content has loaded. Both of + * these values default to 0.25 seconds. + */ +@property (nonatomic, assign) NSTimeInterval loadingViewFadeDelay; +@property (nonatomic, assign) NSTimeInterval loadingViewFadeDuration; + @end diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index e9a8170eb..b12cda4f2 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -25,6 +25,8 @@ #import "RCTWebViewExecutor.h" #import "UIView+React.h" +NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification"; + @interface RCTBridge (RCTRootView) @property (nonatomic, weak, readonly) RCTBridge *batchedBridge; @@ -39,6 +41,8 @@ @interface RCTRootContentView : RCTView +@property (nonatomic, readonly) BOOL contentHasAppeared; + - (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge; @end @@ -64,14 +68,23 @@ _bridge = bridge; _moduleName = moduleName; + _loadingViewFadeDelay = 0.25; + _loadingViewFadeDuration = 0.25; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(javaScriptDidLoad:) name:RCTJavaScriptDidLoadNotification object:_bridge]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(hideLoadingView) + name:RCTContentDidAppearNotification + object:self]; if (!_bridge.batchedBridge.isLoading) { [self bundleFinishedLoading:_bridge.batchedBridge]; } + + [self showLoadingView]; } return self; } @@ -106,6 +119,41 @@ RCT_IMPORT_METHOD(AppRegistry, runApplication) RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) +- (void)setLoadingView:(UIView *)loadingView +{ + _loadingView = loadingView; + if (!_contentView.contentHasAppeared) { + [self showLoadingView]; + } +} + +- (void)showLoadingView +{ + if (_loadingView && !_contentView.contentHasAppeared) { + _loadingView.hidden = NO; + [self addSubview:_loadingView]; + } +} + +- (void)hideLoadingView +{ + if (_loadingView.superview == self && _contentView.contentHasAppeared) { + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_loadingViewFadeDelay * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + + [UIView transitionWithView:self + duration:_loadingViewFadeDuration + options:UIViewAnimationOptionTransitionCrossDissolve + animations:^{ + _loadingView.hidden = YES; + } completion:^(BOOL finished) { + [_loadingView removeFromSuperview]; + }]; + }); + } +} + - (void)javaScriptDidLoad:(NSNotification *)notification { RCTBridge *bridge = notification.userInfo[@"bridge"]; @@ -119,35 +167,31 @@ RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) return; } - /** - * Every root view that is created must have a unique React tag. - * Numbering of these tags goes from 1, 11, 21, 31, etc - * - * NOTE: Since the bridge persists, the RootViews might be reused, so now - * the React tag is assigned every time we load new content. - */ [_contentView removeFromSuperview]; _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds bridge:bridge]; _contentView.backgroundColor = self.backgroundColor; - [self addSubview:_contentView]; + [self insertSubview:_contentView atIndex:0]; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @"rootTag": _contentView.reactTag, @"initialProps": _initialProperties ?: @{}, }; + [bridge enqueueJSCall:@"AppRegistry.runApplication" - args:@[moduleName, appParameters]]; + args:@[moduleName, appParameters]]; }); } - (void)layoutSubviews { [super layoutSubviews]; - if (_contentView) { - _contentView.frame = self.bounds; - } + _contentView.frame = self.bounds; + _loadingView.center = (CGPoint){ + CGRectGetMidX(self.bounds), + CGRectGetMidY(self.bounds) + }; } - (NSNumber *)reactTag @@ -155,6 +199,13 @@ RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) return _contentView.reactTag; } +- (void)contentViewInvalidated +{ + [_contentView removeFromSuperview]; + _contentView = nil; + [self showLoadingView]; +} + - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; @@ -193,6 +244,18 @@ RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) return self; } +- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex +{ + [super insertReactSubview:subview atIndex:atIndex]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (!_contentHasAppeared) { + _contentHasAppeared = YES; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTContentDidAppearNotification + object:self.superview]; + } + }); +} + - (void)setFrame:(CGRect)frame { super.frame = frame; @@ -237,7 +300,7 @@ RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) { if (self.isValid) { self.userInteractionEnabled = NO; - [self removeFromSuperview]; + [(RCTRootView *)self.superview contentViewInvalidated]; [_bridge enqueueJSCall:@"ReactNative.unmountComponentAtNodeAndRemoveContainer" args:@[self.reactTag]]; } diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 09cfd758f..f585edc10 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -420,12 +420,12 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block { - if ([NSThread currentThread] != _javaScriptThread) { - [self performSelector:@selector(executeBlockOnJavaScriptQueue:) - onThread:_javaScriptThread withObject:block waitUntilDone:NO]; - } else { - block(); - } + if ([NSThread currentThread] != _javaScriptThread) { + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread withObject:block waitUntilDone:NO]; + } else { + block(); + } } - (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block