1// Copyright 2018-present 650 Industries. All rights reserved.
2
3#import <EXSplashScreen/EXSplashScreenModule.h>
4#import <EXSplashScreen/EXSplashScreenService.h>
5#import <React/RCTRootView.h>
6#import <ExpoModulesCore/EXAppLifecycleService.h>
7#import <ExpoModulesCore/EXUtilities.h>
8
9@protocol EXSplashScreenUtilService
10
11- (UIViewController *)currentViewController;
12
13@end
14
15@interface EXSplashScreenModule ()
16
17@property (nonatomic, weak) EXModuleRegistry *moduleRegistry;
18@property (nonatomic, weak) id<EXUtilitiesInterface> utilities;
19
20@end
21
22@implementation EXSplashScreenModule
23
24EX_EXPORT_MODULE(ExpoSplashScreen);
25
26- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
27{
28  _moduleRegistry = moduleRegistry;
29  _utilities = [moduleRegistry getModuleImplementingProtocol:@protocol(EXUtilitiesInterface)];
30  [[moduleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] registerAppLifecycleListener:self];
31}
32
33EX_EXPORT_METHOD_AS(hideAsync,
34                    hideWithResolve:(EXPromiseResolveBlock)resolve
35                    reject:(EXPromiseRejectBlock)reject)
36{
37  EX_WEAKIFY(self);
38  dispatch_async(dispatch_get_main_queue(), ^{
39    EX_ENSURE_STRONGIFY(self);
40    UIViewController *currentViewController = [self reactViewController];
41    [[self splashScreenService] hideSplashScreenFor:currentViewController
42                                    successCallback:^(BOOL hasEffect){ resolve(@(hasEffect)); }
43                                    failureCallback:^(NSString *message){ reject(@"ERR_SPLASH_SCREEN_CANNOT_HIDE", message, nil); }];
44  });
45}
46
47EX_EXPORT_METHOD_AS(preventAutoHideAsync,
48                    preventAutoHideWithResolve:(EXPromiseResolveBlock)resolve
49                    reject:(EXPromiseRejectBlock)reject)
50{
51  EX_WEAKIFY(self);
52  dispatch_async(dispatch_get_main_queue(), ^{
53    EX_ENSURE_STRONGIFY(self);
54    UIViewController *currentViewController = [self reactViewController];
55    [[self splashScreenService] preventSplashScreenAutoHideFor:currentViewController
56                                               successCallback:^(BOOL hasEffect){ resolve(@(hasEffect)); }
57                                               failureCallback:^(NSString *message){ reject(@"ERR_SPLASH_SCREEN_CANNOT_PREVENT_AUTOHIDE", message, nil); }];
58  });
59}
60
61# pragma mark - EXAppLifecycleListener
62
63- (void)onAppBackgrounded {}
64
65- (void)onAppForegrounded {}
66
67- (void)onAppContentDidAppear
68{
69  EX_WEAKIFY(self);
70  dispatch_async(dispatch_get_main_queue(), ^{
71    EX_ENSURE_STRONGIFY(self);
72    UIViewController* currentViewController = [self reactViewController];
73    [[self splashScreenService] onAppContentDidAppear:currentViewController];
74  });
75}
76
77- (void)onAppContentWillReload
78{
79  EX_WEAKIFY(self);
80  dispatch_async(dispatch_get_main_queue(), ^{
81    EX_ENSURE_STRONGIFY(self);
82    UIViewController* currentViewController = [self reactViewController];
83    [[self splashScreenService] onAppContentWillReload:currentViewController];
84  });
85}
86
87# pragma mark - internals
88
89/**
90 * Tries to obtain singleton module that is registered as "SplashScreen".
91 * Silent agreement is that registered module conforms to "EXSplashScreenService" protocol.
92 */
93- (id<EXSplashScreenService>)splashScreenService
94{
95  return [self.moduleRegistry getSingletonModuleForName:@"SplashScreen"];
96}
97
98/**
99 * Tries to obtain a reference to the UIViewController for the main RCTRootView
100 * by iterating through all of the application's windows and their viewControllers
101 * until it finds one with a RCTRootView.
102 */
103- (UIViewController *)reactViewController
104{
105  dispatch_assert_queue(dispatch_get_main_queue());
106
107  // first check to see if the host application has a module that provides the reference we want
108  // (this is the case in Expo Go and in the ExpoKit pod used in `expo build` apps)
109  id<EXSplashScreenUtilService> utilService = [_moduleRegistry getSingletonModuleForName:@"Util"];
110  if (utilService != nil) {
111    return [utilService currentViewController];
112  }
113
114  UIViewController *controller = [self viewControllerContainingRCTRootView];
115  if (!controller) {
116    // no RCTRootView was found, so just fall back to the key window's root view controller
117    controller = self.utilities.currentViewController;
118    while ([controller isKindOfClass:[UIAlertController class]] &&
119           controller.presentingViewController != nil) {
120      controller = controller.presentingViewController;
121    }
122    return controller;
123  }
124
125  UIViewController *presentedController = controller.presentedViewController;
126  while (presentedController &&
127         ![presentedController isBeingDismissed] &&
128         ![presentedController isKindOfClass:[UIAlertController class]]) {
129    controller = presentedController;
130    presentedController = controller.presentedViewController;
131  }
132  return controller;
133}
134
135- (nullable UIViewController *)viewControllerContainingRCTRootView
136{
137  NSArray<UIWindow *> *allWindows;
138  if (@available(iOS 13, *)) {
139    NSSet<UIScene *> *allWindowScenes = UIApplication.sharedApplication.connectedScenes;
140    NSMutableArray<UIWindow *> *allForegroundWindows = [NSMutableArray new];
141    for (UIScene *scene in allWindowScenes.allObjects) {
142      if ([scene isKindOfClass:[UIWindowScene class]] && scene.activationState == UISceneActivationStateForegroundActive) {
143        [allForegroundWindows addObjectsFromArray:((UIWindowScene *)scene).windows];
144      }
145    }
146    allWindows = allForegroundWindows;
147  } else {
148    allWindows = UIApplication.sharedApplication.windows;
149  }
150
151  for (UIWindow *window in allWindows) {
152    UIViewController *controller = window.rootViewController;
153    if ([controller.view isKindOfClass:[RCTRootView class]]) {
154      return controller;
155    }
156    UIViewController *presentedController = controller.presentedViewController;
157    while (presentedController && ![presentedController isBeingDismissed]) {
158      if ([presentedController.view isKindOfClass:[RCTRootView class]]) {
159        return presentedController;
160      }
161      controller = presentedController;
162      presentedController = controller.presentedViewController;
163    }
164  }
165
166  // no RCTRootView was found
167  return nil;
168}
169
170@end
171