1// Copyright © 2018 650 Industries. All rights reserved. 2 3#import <ABI48_0_0EXSplashScreen/ABI48_0_0EXSplashScreenService.h> 4#import <ABI48_0_0EXSplashScreen/ABI48_0_0EXSplashScreenViewNativeProvider.h> 5#import <ABI48_0_0ExpoModulesCore/ABI48_0_0EXDefines.h> 6 7static NSString * const kRootViewController = @"rootViewController"; 8static NSString * const kView = @"view"; 9 10@interface ABI48_0_0EXSplashScreenService () 11 12@property (nonatomic, strong) NSMapTable<UIViewController *, ABI48_0_0EXSplashScreenViewController *> *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 ABI48_0_0EXSplashScreenService 27 28ABI48_0_0EX_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:(ABI48_0_0EXSplashScreenOptions)options 40{ 41 id<ABI48_0_0EXSplashScreenViewProvider> splashScreenViewProvider = [ABI48_0_0EXSplashScreenViewNativeProvider new]; 42 return [self showSplashScreenFor:viewController 43 options:options 44 splashScreenViewProvider:splashScreenViewProvider 45 successCallback:^{} 46 failureCallback:^(NSString *message){ ABI48_0_0EXLogWarn(@"%@", message); }]; 47} 48 49 50- (void)showSplashScreenFor:(UIViewController *)viewController 51 options:(ABI48_0_0EXSplashScreenOptions)options 52 splashScreenViewProvider:(id<ABI48_0_0EXSplashScreenViewProvider>)splashScreenViewProvider 53 successCallback:(void (^)(void))successCallback 54 failureCallback:(void (^)(NSString * _Nonnull))failureCallback 55{ 56 if ((options & ABI48_0_0EXSplashScreenForceShow) == 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 ABI48_0_0EXSplashScreenViewController *splashScreenController = [[ABI48_0_0EXSplashScreenViewController 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:(ABI48_0_0EXSplashScreenOptions)options 75 splashScreenController:(ABI48_0_0EXSplashScreenViewController *)splashScreenController 76 successCallback:(void (^)(void))successCallback 77 failureCallback:(void (^)(NSString * _Nonnull))failureCallback 78{ 79 if ((options & ABI48_0_0EXSplashScreenForceShow) == 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:(ABI48_0_0EXSplashScreenOptions)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:(ABI48_0_0EXSplashScreenOptions)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 if ([self isAppActive] && ![self.splashScreenControllers objectForKey:viewController]) { 118 ABI48_0_0EXLogWarn(@"No native splash screen registered for given view controller. Call 'SplashScreen.show' for given view controller first."); 119 } 120 BOOL needsHide = [[self.splashScreenControllers objectForKey:viewController] needsHideOnAppContentDidAppear]; 121 if (needsHide) { 122 [self hideSplashScreenFor:viewController 123 options:ABI48_0_0EXSplashScreenDefault 124 successCallback:^(BOOL hasEffect){} 125 failureCallback:^(NSString *message){}]; 126 } 127} 128 129- (void)onAppContentWillReload:(UIViewController *)viewController 130{ 131 if ([self isAppActive] && ![self.splashScreenControllers objectForKey:viewController]) { 132 ABI48_0_0EXLogWarn(@"No native splash screen registered for given view controller. Call 'SplashScreen.show' for given view controller first."); 133 } 134 BOOL needsShow = [[self.splashScreenControllers objectForKey:viewController] needsShowOnAppContentWillReload]; 135 if (needsShow) { 136 // For reloading apps, specify `ABI48_0_0EXSplashScreenForceShow` to show splash screen again 137 [self showSplashScreenFor:viewController 138 options:ABI48_0_0EXSplashScreenForceShow 139 splashScreenController:[self.splashScreenControllers objectForKey:viewController] 140 successCallback:^{} 141 failureCallback:^(NSString *message){}]; 142 } 143} 144 145- (BOOL)isAppActive { 146 return UIApplication.sharedApplication.applicationState == UIApplicationStateActive; 147} 148 149# pragma mark - UIApplicationDelegate 150 151- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 152{ 153 UIViewController *rootViewController = [[application keyWindow] rootViewController]; 154 if (rootViewController) { 155 [self showSplashScreenFor:rootViewController options:ABI48_0_0EXSplashScreenDefault]; 156 } 157 158 [self addRootViewControllerListener]; 159 return YES; 160} 161 162# pragma mark - RootViewController KVO 163 164- (void)addRootViewControllerListener 165{ 166 NSAssert([NSThread isMainThread], @"Method must be called on main thread"); 167 if (self.observingRootViewController == nil) { 168 UIViewController *rootViewController = UIApplication.sharedApplication.keyWindow.rootViewController; 169 170 [UIApplication.sharedApplication.keyWindow addObserver:self 171 forKeyPath:kRootViewController 172 options:NSKeyValueObservingOptionNew 173 context:nil]; 174 175 [rootViewController addObserver:self forKeyPath:kView options:NSKeyValueObservingOptionNew context:nil]; 176 self.observingRootViewController = rootViewController; 177 } 178} 179 180- (void)removeRootViewControllerListener 181{ 182 NSAssert([NSThread isMainThread], @"Method must be called on main thread"); 183 if (self.observingRootViewController != nil) { 184 UIWindow *window = self.observingRootViewController.view.window; 185 [window removeObserver:self forKeyPath:kRootViewController context:nil]; 186 [self.observingRootViewController removeObserver:self forKeyPath:kView context:nil]; 187 self.observingRootViewController = nil; 188 } 189} 190 191- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 192{ 193 if (object == UIApplication.sharedApplication.keyWindow && [keyPath isEqualToString:kRootViewController]) { 194 UIViewController *newRootViewController = change[@"new"]; 195 // For unknown reasons, this function may be sometimes called twice with the same changes. 196 // What leads to warnings like this one: `'SplashScreen.show' has already been called for given view controller`. 197 // To prevent this weird behaviour, we check if the value was really changed. 198 if (newRootViewController != nil && newRootViewController != self.observingRootViewController) { 199 [self removeRootViewControllerListener]; 200 [self showSplashScreenFor:newRootViewController options:ABI48_0_0EXSplashScreenDefault]; 201 [self addRootViewControllerListener]; 202 } 203 } 204 if (object == UIApplication.sharedApplication.keyWindow.rootViewController && [keyPath isEqualToString:kView]) { 205 UIView *newView = change[@"new"]; 206 if (newView != nil && [newView.nextResponder isKindOfClass:[UIViewController class]]) { 207 UIViewController *viewController = (UIViewController *)newView.nextResponder; 208 // To show splash screen as soon as possible, we do not wait for hiding callback and call showSplashScreen immediately. 209 // GCD main queue should keep the calls in sequence. 210 [self hideSplashScreenFor:viewController options:ABI48_0_0EXSplashScreenDefault successCallback:^(BOOL hasEffect){} failureCallback:^(NSString *message){}]; 211 [self.splashScreenControllers removeObjectForKey:viewController]; 212 [self showSplashScreenFor:viewController options:ABI48_0_0EXSplashScreenDefault]; 213 } 214 } 215} 216 217@end 218