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