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                                            options:EXSplashScreenDefault
43                                    successCallback:^(BOOL hasEffect){ resolve(@(hasEffect)); }
44                                    failureCallback:^(NSString *message){ reject(@"ERR_SPLASH_SCREEN_CANNOT_HIDE", message, nil); }];
45  });
46}
47
48EX_EXPORT_METHOD_AS(preventAutoHideAsync,
49                    preventAutoHideWithResolve:(EXPromiseResolveBlock)resolve
50                    reject:(EXPromiseRejectBlock)reject)
51{
52  EX_WEAKIFY(self);
53  dispatch_async(dispatch_get_main_queue(), ^{
54    EX_ENSURE_STRONGIFY(self);
55    UIViewController *currentViewController = [self reactViewController];
56    [[self splashScreenService] preventSplashScreenAutoHideFor:currentViewController
57                                                       options:EXSplashScreenDefault
58                                               successCallback:^(BOOL hasEffect){ resolve(@(hasEffect)); }
59                                               failureCallback:^(NSString *message){ reject(@"ERR_SPLASH_SCREEN_CANNOT_PREVENT_AUTOHIDE", message, nil); }];
60  });
61}
62
63# pragma mark - EXAppLifecycleListener
64
65- (void)onAppBackgrounded {}
66
67- (void)onAppForegrounded {}
68
69- (void)onAppContentDidAppear
70{
71  EX_WEAKIFY(self);
72  dispatch_async(dispatch_get_main_queue(), ^{
73    EX_ENSURE_STRONGIFY(self);
74    UIViewController* currentViewController = [self reactViewController];
75    [[self splashScreenService] onAppContentDidAppear:currentViewController];
76  });
77}
78
79- (void)onAppContentWillReload
80{
81  EX_WEAKIFY(self);
82  dispatch_async(dispatch_get_main_queue(), ^{
83    EX_ENSURE_STRONGIFY(self);
84    UIViewController* currentViewController = [self reactViewController];
85    [[self splashScreenService] onAppContentWillReload:currentViewController];
86  });
87}
88
89# pragma mark - internals
90
91/**
92 * Tries to obtain singleton module that is registered as "SplashScreen".
93 * Silent agreement is that registered module conforms to "EXSplashScreenService" protocol.
94 */
95- (id<EXSplashScreenService>)splashScreenService
96{
97  return [self.moduleRegistry getSingletonModuleForName:@"SplashScreen"];
98}
99
100/**
101 * Tries to obtain a reference to the UIViewController for the main RCTRootView
102 * by iterating through all of the application's windows and their viewControllers
103 * until it finds one with a RCTRootView.
104 */
105- (UIViewController *)reactViewController
106{
107  dispatch_assert_queue(dispatch_get_main_queue());
108
109  // first check to see if the host application has a module that provides the reference we want
110  // (this is the case in Expo Go and in the ExpoKit pod used in `expo build` apps)
111  id<EXSplashScreenUtilService> utilService = [_moduleRegistry getSingletonModuleForName:@"Util"];
112  if (utilService != nil) {
113    return [utilService currentViewController];
114  }
115
116  UIViewController *controller = [self viewControllerContainingRCTRootView];
117  if (!controller) {
118    // no RCTRootView was found, so just fall back to the key window's root view controller
119    controller = self.utilities.currentViewController;
120    while ([controller isKindOfClass:[UIAlertController class]] &&
121           controller.presentingViewController != nil) {
122      controller = controller.presentingViewController;
123    }
124    return controller;
125  }
126
127  UIViewController *presentedController = controller.presentedViewController;
128  while (presentedController &&
129         ![presentedController isBeingDismissed] &&
130         ![presentedController isKindOfClass:[UIAlertController class]]) {
131    controller = presentedController;
132    presentedController = controller.presentedViewController;
133  }
134  return controller;
135}
136
137- (nullable UIViewController *)viewControllerContainingRCTRootView
138{
139  NSArray<UIWindow *> *allWindows;
140  if (@available(iOS 13, *)) {
141    NSSet<UIScene *> *allWindowScenes = UIApplication.sharedApplication.connectedScenes;
142    NSMutableArray<UIWindow *> *allForegroundWindows = [NSMutableArray new];
143    for (UIScene *scene in allWindowScenes.allObjects) {
144      if ([scene isKindOfClass:[UIWindowScene class]] && scene.activationState == UISceneActivationStateForegroundActive) {
145        [allForegroundWindows addObjectsFromArray:((UIWindowScene *)scene).windows];
146      }
147    }
148    allWindows = allForegroundWindows;
149  } else {
150    allWindows = UIApplication.sharedApplication.windows;
151  }
152
153  for (UIWindow *window in allWindows) {
154    UIViewController *controller = window.rootViewController;
155    if ([controller.view isKindOfClass:[RCTRootView class]]) {
156      return controller;
157    }
158    UIViewController *presentedController = controller.presentedViewController;
159    while (presentedController && ![presentedController isBeingDismissed]) {
160      if ([presentedController.view isKindOfClass:[RCTRootView class]]) {
161        return presentedController;
162      }
163      controller = presentedController;
164      presentedController = controller.presentedViewController;
165    }
166  }
167
168  // no RCTRootView was found
169  return nil;
170}
171
172@end
173