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