1// Copyright © 2018 650 Industries. All rights reserved. 2 3#import <EXSplashScreen/EXSplashScreenService.h> 4#import <EXSplashScreen/EXSplashScreenViewNativeProvider.h> 5#import <ExpoModulesCore/EXDefines.h> 6 7static NSString * const kRootViewController = @"rootViewController"; 8static NSString * const kView = @"view"; 9 10@interface EXSplashScreenService () 11 12@property (nonatomic, strong) NSMapTable<UIViewController *, EXSplashScreenViewController *> *splashScreenControllers; 13/** 14 * This module holds a reference to rootViewController acting as a flag to indicate KVO is enabled. 15 * When KVO is enabled, actually we are observing two targets and re-show splash screen if targets changed: 16 * - `keyWindow.rootViewController`: it is for expo-dev-client which replaced it in startup. 17 * - `rootViewController.rootView`: it is for expo-updates which replaced it in startup. 18 * 19 * If `rootViewController` is changed, we also need the old `rootViewController` to unregister rootView KVO. 20 * That's why we keep a weak reference here but not a boolean flag. 21 */ 22@property (nonatomic, weak) UIViewController *observingRootViewController; 23 24@end 25 26@implementation EXSplashScreenService 27 28EX_REGISTER_SINGLETON_MODULE(SplashScreen); 29 30- (instancetype)init 31{ 32 if (self = [super init]) { 33 _splashScreenControllers = [NSMapTable weakToStrongObjectsMapTable]; 34 } 35 return self; 36} 37 38- (void)showSplashScreenFor:(UIViewController *)viewController 39 options:(EXSplashScreenOptions)options 40{ 41 id<EXSplashScreenViewProvider> splashScreenViewProvider = [EXSplashScreenViewNativeProvider new]; 42 return [self showSplashScreenFor:viewController 43 options:options 44 splashScreenViewProvider:splashScreenViewProvider 45 successCallback:^{} 46 failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }]; 47} 48 49 50- (void)showSplashScreenFor:(UIViewController *)viewController 51 options:(EXSplashScreenOptions)options 52 splashScreenViewProvider:(id<EXSplashScreenViewProvider>)splashScreenViewProvider 53 successCallback:(void (^)(void))successCallback 54 failureCallback:(void (^)(NSString * _Nonnull))failureCallback 55{ 56 if ((options & EXSplashScreenForceShow) == 0 && [self.splashScreenControllers objectForKey:viewController]) { 57 return failureCallback(@"'SplashScreen.show' has already been called for given view controller."); 58 } 59 60 61 UIView *rootView = viewController.view; 62 UIView *splashScreenView = [splashScreenViewProvider createSplashScreenView]; 63 EXSplashScreenViewController *splashScreenController = [[EXSplashScreenViewController alloc] initWithRootView:rootView 64 splashScreenView:splashScreenView]; 65 66 [self showSplashScreenFor:viewController 67 options:options 68 splashScreenController:splashScreenController 69 successCallback:successCallback 70 failureCallback:failureCallback]; 71} 72 73- (void)showSplashScreenFor:(UIViewController *)viewController 74 options:(EXSplashScreenOptions)options 75 splashScreenController:(EXSplashScreenViewController *)splashScreenController 76 successCallback:(void (^)(void))successCallback 77 failureCallback:(void (^)(NSString * _Nonnull))failureCallback 78{ 79 if ((options & EXSplashScreenForceShow) == 0 && [self.splashScreenControllers objectForKey:viewController]) { 80 return failureCallback(@"'SplashScreen.show' has already been called for given view controller."); 81 } 82 83 [self.splashScreenControllers setObject:splashScreenController forKey:viewController]; 84 [[self.splashScreenControllers objectForKey:viewController] showWithCallback:successCallback 85 failureCallback:failureCallback]; 86} 87 88- (void)preventSplashScreenAutoHideFor:(UIViewController *)viewController 89 options:(EXSplashScreenOptions)options 90 successCallback:(void (^)(BOOL hasEffect))successCallback 91 failureCallback:(void (^)(NSString * _Nonnull))failureCallback 92{ 93 if (![self.splashScreenControllers objectForKey:viewController]) { 94 return failureCallback(@"No native splash screen registered for given view controller. Call 'SplashScreen.show' for given view controller first."); 95 } 96 97 return [[self.splashScreenControllers objectForKey:viewController] preventAutoHideWithCallback:successCallback 98 failureCallback:failureCallback]; 99} 100 101- (void)hideSplashScreenFor:(UIViewController *)viewController 102 options:(EXSplashScreenOptions)options 103 successCallback:(void (^)(BOOL hasEffect))successCallback 104 failureCallback:(void (^)(NSString * _Nonnull))failureCallback 105{ 106 if (![self.splashScreenControllers objectForKey:viewController]) { 107 return failureCallback(@"No native splash screen registered for given view controller. Call 'SplashScreen.show' for given view controller first."); 108 } 109 [self removeRootViewControllerListener]; 110 111 return [[self.splashScreenControllers objectForKey:viewController] hideWithCallback:successCallback 112 failureCallback:failureCallback]; 113} 114 115- (void)onAppContentDidAppear:(UIViewController *)viewController 116{ 117 BOOL needsHide = [[self.splashScreenControllers objectForKey:viewController] needsHideOnAppContentDidAppear]; 118 if (needsHide) { 119 [self hideSplashScreenFor:viewController 120 options:EXSplashScreenDefault 121 successCallback:^(BOOL hasEffect){} 122 failureCallback:^(NSString *message){}]; 123 } 124} 125 126- (void)onAppContentWillReload:(UIViewController *)viewController 127{ 128 BOOL needsShow = [[self.splashScreenControllers objectForKey:viewController] needsShowOnAppContentWillReload]; 129 if (needsShow) { 130 // For reloading apps, specify `EXSplashScreenForceShow` to show splash screen again 131 [self showSplashScreenFor:viewController 132 options:EXSplashScreenForceShow 133 splashScreenController:[self.splashScreenControllers objectForKey:viewController] 134 successCallback:^{} 135 failureCallback:^(NSString *message){}]; 136 } 137} 138 139- (BOOL)isAppActive { 140 return UIApplication.sharedApplication.applicationState == UIApplicationStateActive; 141} 142 143# pragma mark - UIApplicationDelegate 144 145- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 146{ 147 UIViewController *rootViewController = [[application keyWindow] rootViewController]; 148 if (rootViewController) { 149 [self showSplashScreenFor:rootViewController options:EXSplashScreenDefault]; 150 } 151 152 [self addRootViewControllerListener]; 153 return YES; 154} 155 156# pragma mark - RootViewController KVO 157 158- (void)addRootViewControllerListener 159{ 160 NSAssert([NSThread isMainThread], @"Method must be called on main thread"); 161 if (self.observingRootViewController == nil) { 162 UIViewController *rootViewController = UIApplication.sharedApplication.keyWindow.rootViewController; 163 164 [UIApplication.sharedApplication.keyWindow addObserver:self 165 forKeyPath:kRootViewController 166 options:NSKeyValueObservingOptionNew 167 context:nil]; 168 169 [rootViewController addObserver:self forKeyPath:kView options:NSKeyValueObservingOptionNew context:nil]; 170 self.observingRootViewController = rootViewController; 171 } 172} 173 174- (void)removeRootViewControllerListener 175{ 176 NSAssert([NSThread isMainThread], @"Method must be called on main thread"); 177 if (self.observingRootViewController != nil) { 178 UIWindow *window = self.observingRootViewController.view.window; 179 [window removeObserver:self forKeyPath:kRootViewController context:nil]; 180 [self.observingRootViewController removeObserver:self forKeyPath:kView context:nil]; 181 self.observingRootViewController = nil; 182 } 183} 184 185- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 186{ 187 if (object == UIApplication.sharedApplication.keyWindow && [keyPath isEqualToString:kRootViewController]) { 188 UIViewController *newRootViewController = change[@"new"]; 189 // For unknown reasons, this function may be sometimes called twice with the same changes. 190 // What leads to warnings like this one: `'SplashScreen.show' has already been called for given view controller`. 191 // To prevent this weird behaviour, we check if the value was really changed. 192 if (newRootViewController != nil && newRootViewController != self.observingRootViewController) { 193 [self removeRootViewControllerListener]; 194 [self showSplashScreenFor:newRootViewController options:EXSplashScreenDefault]; 195 [self addRootViewControllerListener]; 196 } 197 } 198 if (object == UIApplication.sharedApplication.keyWindow.rootViewController && [keyPath isEqualToString:kView]) { 199 UIView *newView = change[@"new"]; 200 if (newView != nil && [newView.nextResponder isKindOfClass:[UIViewController class]]) { 201 UIViewController *viewController = (UIViewController *)newView.nextResponder; 202 // To show splash screen as soon as possible, we do not wait for hiding callback and call showSplashScreen immediately. 203 // GCD main queue should keep the calls in sequence. 204 [self hideSplashScreenFor:viewController options:EXSplashScreenDefault successCallback:^(BOOL hasEffect){} failureCallback:^(NSString *message){}]; 205 [self.splashScreenControllers removeObjectForKey:viewController]; 206 [self showSplashScreenFor:viewController options:EXSplashScreenDefault]; 207 } 208 } 209} 210 211@end 212