1// Copyright 2015-present 650 Industries. All rights reserved. 2 3@import UIKit; 4 5#import "EXAnalytics.h" 6#import "EXAbstractLoader.h" 7#import "EXAppViewController.h" 8#import "EXAppLoadingProgressWindowController.h" 9#import "EXAppLoadingCancelView.h" 10#import "EXManagedAppSplashScreenViewProvider.h" 11#import "EXManagedAppSplashScreenConfigurationBuilder.h" 12#import "EXManagedAppSplashScreenViewController.h" 13#import "EXHomeAppSplashScreenViewProvider.h" 14#import "EXEnvironment.h" 15#import "EXErrorRecoveryManager.h" 16#import "EXErrorView.h" 17#import "EXFileDownloader.h" 18#import "EXKernel.h" 19#import "EXKernelUtil.h" 20#import "EXReactAppManager.h" 21#import "EXVersions.h" 22#import "EXUpdatesManager.h" 23#import "EXUtil.h" 24 25#import <EXSplashScreen/EXSplashScreenService.h> 26#import <React/RCTUtils.h> 27#import <ExpoModulesCore/EXModuleRegistryProvider.h> 28 29#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 30#import <EXScreenOrientation/EXScreenOrientationRegistry.h> 31#endif 32 33#import <React/RCTAppearance.h> 34#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI48_0_0React/ABI48_0_0RCTAppearance.h>) 35#import <ABI48_0_0React/ABI48_0_0RCTAppearance.h> 36#endif 37#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI47_0_0React/ABI47_0_0RCTAppearance.h>) 38#import <ABI47_0_0React/ABI47_0_0RCTAppearance.h> 39#endif 40#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI46_0_0React/ABI46_0_0RCTAppearance.h>) 41#import <ABI46_0_0React/ABI46_0_0RCTAppearance.h> 42#endif 43 44#if defined(EX_DETACHED) 45#import "ExpoKit-Swift.h" 46#else 47#import "Expo_Go-Swift.h" 48#endif // defined(EX_DETACHED) 49 50@import EXManifests; 51 52#define EX_INTERFACE_ORIENTATION_USE_MANIFEST 0 53 54// when we encounter an error and auto-refresh, we may actually see a series of errors. 55// we only want to trigger refresh once, so we debounce refresh on a timer. 56const CGFloat kEXAutoReloadDebounceSeconds = 0.1; 57 58// in development only, some errors can happen before we even start loading 59// (e.g. certain packager errors, such as an invalid bundle url) 60// and we want to make sure not to cover the error with a loading view or other chrome. 61const CGFloat kEXDevelopmentErrorCoolDownSeconds = 0.1; 62 63// copy of RNScreens protocol 64@protocol EXKernelRNSScreenWindowTraits 65 66+ (BOOL)shouldAskScreensForScreenOrientationInViewController:(UIViewController *)vc; 67 68@end 69 70NS_ASSUME_NONNULL_BEGIN 71 72@interface EXAppViewController () 73 <EXReactAppManagerUIDelegate, EXAppLoaderDelegate, EXErrorViewDelegate, EXAppLoadingCancelViewDelegate> 74 75@property (nonatomic, assign) BOOL isLoading; 76@property (atomic, assign) BOOL isBridgeAlreadyLoading; 77@property (nonatomic, weak) EXKernelAppRecord *appRecord; 78@property (nonatomic, strong) EXErrorView *errorView; 79@property (nonatomic, strong) NSTimer *tmrAutoReloadDebounce; 80@property (nonatomic, strong) NSDate *dtmLastFatalErrorShown; 81@property (nonatomic, strong) NSMutableArray<UIViewController *> *backgroundedControllers; 82 83@property (nonatomic, assign) BOOL isStandalone; 84@property (nonatomic, assign) BOOL isHomeApp; 85 86/* 87 * Controller for handling all messages from bundler/fetcher. 88 * It shows another UIWindow with text and percentage progress. 89 * Enabled only in managed workflow or home when in development mode. 90 * It should appear once manifest is fetched. 91 */ 92@property (nonatomic, strong, nonnull) EXAppLoadingProgressWindowController *appLoadingProgressWindowController; 93 94/** 95 * SplashScreenViewProvider that is used only in managed workflow app. 96 * Managed app does not need any specific SplashScreenViewProvider as it uses generic one povided by the SplashScreen module. 97 * See also EXHomeAppSplashScreenViewProvider in self.viewDidLoad 98 */ 99@property (nonatomic, strong, nullable) EXManagedAppSplashScreenViewProvider *managedAppSplashScreenViewProvider; 100@property (nonatomic, strong, nullable) EXManagedAppSplashScreenViewController *managedSplashScreenController; 101 102/* 103 * This view is available in managed apps run in Expo Go only. 104 * It is shown only before any managed app manifest is delivered by the app loader. 105 */ 106@property (nonatomic, strong, nullable) EXAppLoadingCancelView *appLoadingCancelView; 107 108@end 109 110@implementation EXAppViewController 111 112#pragma mark - Lifecycle 113 114- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record 115{ 116 if (self = [super init]) { 117 _appRecord = record; 118 _isStandalone = [EXEnvironment sharedEnvironment].isDetached; 119 } 120 return self; 121} 122 123- (void)dealloc 124{ 125 [self _invalidateRecoveryTimer]; 126 [[NSNotificationCenter defaultCenter] removeObserver:self]; 127} 128 129- (void)viewDidLoad 130{ 131 [super viewDidLoad]; 132 133 // EXKernel.appRegistry.homeAppRecord does not contain any homeAppRecord until this point, 134 // therefore we cannot move this property initialization to the constructor/initializer 135 _isHomeApp = _appRecord == [EXKernel sharedInstance].appRegistry.homeAppRecord; 136 137 // show LoadingCancelView in managed apps only 138 if (!self.isStandalone && !self.isHomeApp) { 139 self.appLoadingCancelView = [EXAppLoadingCancelView new]; 140 // if home app is available then LoadingCancelView can show `go to home` button 141 if ([EXKernel sharedInstance].appRegistry.homeAppRecord) { 142 self.appLoadingCancelView.delegate = self; 143 } 144 [self.view addSubview:self.appLoadingCancelView]; 145 [self.view bringSubviewToFront:self.appLoadingCancelView]; 146 } 147 148 // show LoadingProgressWindow in the development client for all apps other than production home 149 BOOL isProductionHomeApp = self.isHomeApp && ![EXEnvironment sharedEnvironment].isDebugXCodeScheme; 150 self.appLoadingProgressWindowController = [[EXAppLoadingProgressWindowController alloc] initWithEnabled:!self.isStandalone && !isProductionHomeApp]; 151 152 // show SplashScreen in standalone apps and home app only 153 // SplashScreen for managed is shown once the manifest is available 154 if (self.isHomeApp) { 155 EXHomeAppSplashScreenViewProvider *homeAppSplashScreenViewProvider = [EXHomeAppSplashScreenViewProvider new]; 156 [self _showSplashScreenWithProvider:homeAppSplashScreenViewProvider]; 157 } else if (self.isStandalone) { 158 [self _showSplashScreenWithProvider:[EXSplashScreenViewNativeProvider new]]; 159 } 160 161 self.view.backgroundColor = [UIColor whiteColor]; 162 _appRecord.appManager.delegate = self; 163 self.isLoading = YES; 164} 165 166- (void)viewDidAppear:(BOOL)animated 167{ 168 [super viewDidAppear:animated]; 169 if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) { 170 _appRecord.appLoader.delegate = self; 171 _appRecord.appLoader.dataSource = _appRecord.appManager; 172 [self refresh]; 173 } 174} 175 176- (BOOL)shouldAutorotate 177{ 178 return YES; 179} 180 181- (void)viewWillLayoutSubviews 182{ 183 [super viewWillLayoutSubviews]; 184 if (_appLoadingCancelView) { 185 _appLoadingCancelView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 186 } 187 if (_contentView) { 188 _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 189 } 190} 191 192- (void)viewWillDisappear:(BOOL)animated 193{ 194 [_appLoadingProgressWindowController hide]; 195 [super viewWillDisappear:animated]; 196} 197 198/** 199 * Force presented view controllers to use the same user interface style. 200 */ 201- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion 202{ 203 [super presentViewController:viewControllerToPresent animated:flag completion:completion]; 204 [self _overrideUserInterfaceStyleOf:viewControllerToPresent]; 205} 206 207/** 208 * Force child view controllers to use the same user interface style. 209 */ 210- (void)addChildViewController:(UIViewController *)childController 211{ 212 [super addChildViewController:childController]; 213 [self _overrideUserInterfaceStyleOf:childController]; 214} 215 216#pragma mark - Public 217 218- (void)maybeShowError:(NSError *)error 219{ 220 self.isLoading = NO; 221 if ([self _willAutoRecoverFromError:error]) { 222 return; 223 } 224 if (error && ![error isKindOfClass:[NSError class]]) { 225#if DEBUG 226 NSAssert(NO, @"AppViewController error handler was called on an object that isn't an NSError"); 227#endif 228 return; 229 } 230 231 // we don't ever want to show any Expo UI in a production standalone app, so hard crash 232 if ([EXEnvironment sharedEnvironment].isDetached && ![_appRecord.appManager enablesDeveloperTools]) { 233 NSException *e = [NSException exceptionWithName:@"ExpoFatalError" 234 reason:[NSString stringWithFormat:@"Expo encountered a fatal error: %@", [error localizedDescription]] 235 userInfo:@{NSUnderlyingErrorKey: error}]; 236 @throw e; 237 } 238 239 NSString *domain = (error && error.domain) ? error.domain : @""; 240 BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:NSURLErrorDomain] || [domain isEqualToString:EXNetworkErrorDomain]); 241 242 if (isNetworkError) { 243 // show a human-readable reachability error 244 dispatch_async(dispatch_get_main_queue(), ^{ 245 [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; 246 }); 247 } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { 248 // RCTRedBox already handled this 249 } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { 250 // RCTRedBox already handled this 251 } else { 252 dispatch_async(dispatch_get_main_queue(), ^{ 253 [self _showErrorWithType:kEXFatalErrorTypeException error:error]; 254 }); 255 } 256} 257 258- (void)refresh 259{ 260 self.isLoading = YES; 261 self.isBridgeAlreadyLoading = NO; 262 [self _invalidateRecoveryTimer]; 263 [_appRecord.appLoader request]; 264} 265 266- (void)reloadFromCache 267{ 268 self.isLoading = YES; 269 self.isBridgeAlreadyLoading = NO; 270 [self _invalidateRecoveryTimer]; 271 [_appRecord.appLoader requestFromCache]; 272} 273 274- (bool)_readSupportsRTLFromManifest:(EXManifestsManifest *)manifest 275{ 276 return manifest.supportsRTL; 277} 278 279- (void)appStateDidBecomeActive 280{ 281 if (_isHomeApp) { 282 [EXTextDirectionController setSupportsRTL:false]; 283 } else if(_appRecord.appLoader.manifest != nil) { 284 [EXTextDirectionController setSupportsRTL:[self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest]]; 285 } 286 dispatch_async(dispatch_get_main_queue(), ^{ 287 // Reset the root view background color and window color if we switch between Expo home and project 288 [self _setBackgroundColor]; 289 }); 290} 291 292- (void)appStateDidBecomeInactive 293{ 294} 295 296- (void)_rebuildBridge 297{ 298 if (!self.isBridgeAlreadyLoading) { 299 self.isBridgeAlreadyLoading = YES; 300 dispatch_async(dispatch_get_main_queue(), ^{ 301 [self _overrideUserInterfaceStyleOf:self]; 302 [self _overrideAppearanceModuleBehaviour]; 303 [self _invalidateRecoveryTimer]; 304 [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:self.appRecord]; 305 [self.appRecord.appManager rebuildBridge]; 306 }); 307 } 308} 309 310- (void)foregroundControllers 311{ 312 if (_backgroundedControllers != nil) { 313 __block UIViewController *parentController = self; 314 315 [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) { 316 [parentController presentViewController:viewController animated:NO completion:nil]; 317 parentController = viewController; 318 }]; 319 320 _backgroundedControllers = nil; 321 } 322} 323 324- (void)backgroundControllers 325{ 326 UIViewController *childController = [self presentedViewController]; 327 328 if (childController != nil) { 329 if (_backgroundedControllers == nil) { 330 _backgroundedControllers = [NSMutableArray new]; 331 } 332 333 while (childController != nil) { 334 [_backgroundedControllers addObject:childController]; 335 childController = childController.presentedViewController; 336 } 337 } 338} 339 340/** 341 * In managed app we expect two kinds of manifest: 342 * - optimistic one (served from cache) 343 * - actual one served when app is fetched. 344 * For each of them we should show SplashScreen, 345 * therefore for any consecutive SplashScreen.show call we just reconfigure what's already visible. 346 * In HomeApp or standalone apps this function is no-op as SplashScreen is managed differently. 347 */ 348- (void)_showOrReconfigureManagedAppSplashScreen:(EXManifestsManifest *)manifest 349{ 350 if (_isStandalone || _isHomeApp) { 351 return; 352 } 353 if (!_managedAppSplashScreenViewProvider) { 354 _managedAppSplashScreenViewProvider = [[EXManagedAppSplashScreenViewProvider alloc] initWithManifest:manifest]; 355 356 [self _showManagedSplashScreenWithProvider:_managedAppSplashScreenViewProvider]; 357 } else { 358 [_managedAppSplashScreenViewProvider updateSplashScreenViewWithManifest:manifest]; 359 } 360} 361 362- (void)_showCachedExperienceAlert 363{ 364 if (self.isStandalone || self.isHomeApp) { 365 return; 366 } 367 368 dispatch_async(dispatch_get_main_queue(), ^{ 369 UIAlertController *alert = [UIAlertController 370 alertControllerWithTitle:@"Using a cached project" 371 message:@"If you did not intend to use a cached project, check your network connection and reload." 372 preferredStyle:UIAlertControllerStyleAlert]; 373 [alert addAction:[UIAlertAction actionWithTitle:@"Reload" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 374 [self refresh]; 375 }]]; 376 [alert addAction:[UIAlertAction actionWithTitle:@"Use cache" style:UIAlertActionStyleCancel handler:nil]]; 377 [self presentViewController:alert animated:YES completion:nil]; 378 }); 379} 380 381- (void)_setLoadingViewStatusIfEnabledFromAppLoader:(EXAbstractLoader *)appLoader 382{ 383 if (appLoader.shouldShowRemoteUpdateStatus) { 384 [self.appLoadingProgressWindowController updateStatus:appLoader.remoteUpdateStatus]; 385 } else { 386 [self.appLoadingProgressWindowController hide]; 387 } 388} 389 390- (void)_showSplashScreenWithProvider:(id<EXSplashScreenViewProvider>)provider 391{ 392 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 393 394 // EXSplashScreenService presents a splash screen on a root view controller 395 // at the start of the app. Since we want the EXAppViewController to manage 396 // the lifecycle of the splash screen we need to: 397 // 1. present the splash screen on EXAppViewController 398 // 2. hide the splash screen of root view controller 399 // Disclaimer: 400 // there's only one root view controller, but possibly many EXAppViewControllers 401 // (in Expo Go: one project -> one EXAppViewController) 402 // and we want to hide SplashScreen only once for the root view controller, hence the "once" 403 static dispatch_once_t once; 404 void (^hideRootViewControllerSplashScreen)(void) = ^void() { 405 dispatch_once(&once, ^{ 406 UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; 407 [splashScreenService hideSplashScreenFor:rootViewController 408 options:EXSplashScreenDefault 409 successCallback:^(BOOL hasEffect){} 410 failureCallback:^(NSString * _Nonnull message) { 411 EXLogWarn(@"Hiding splash screen from root view controller did not succeed: %@", message); 412 }]; 413 }); 414 }; 415 416 EX_WEAKIFY(self); 417 dispatch_async(dispatch_get_main_queue(), ^{ 418 EX_ENSURE_STRONGIFY(self); 419 [splashScreenService showSplashScreenFor:self 420 options:EXSplashScreenDefault 421 splashScreenViewProvider:provider 422 successCallback:hideRootViewControllerSplashScreen 423 failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }]; 424 }); 425} 426 427- (void)_showManagedSplashScreenWithProvider:(id<EXSplashScreenViewProvider>)provider 428{ 429 430 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 431 432 EX_WEAKIFY(self); 433 dispatch_async(dispatch_get_main_queue(), ^{ 434 EX_ENSURE_STRONGIFY(self); 435 436 UIView *rootView = self.view; 437 UIView *splashScreenView = [provider createSplashScreenView]; 438 self.managedSplashScreenController = [[EXManagedAppSplashScreenViewController alloc] initWithRootView:rootView 439 splashScreenView:splashScreenView]; 440 [splashScreenService showSplashScreenFor:self 441 options:EXSplashScreenDefault 442 splashScreenController:self.managedSplashScreenController 443 successCallback:^{} 444 failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }]; 445 }); 446 447} 448 449- (void)hideLoadingProgressWindow 450{ 451 [self.appLoadingProgressWindowController hide]; 452 if (self.managedSplashScreenController) { 453 [self.managedSplashScreenController startSplashScreenVisibleTimer]; 454 } 455} 456 457#pragma mark - EXAppLoaderDelegate 458 459- (void)appLoader:(EXAbstractLoader *)appLoader didLoadOptimisticManifest:(EXManifestsManifest *)manifest 460{ 461 if (_appLoadingCancelView) { 462 EX_WEAKIFY(self); 463 dispatch_async(dispatch_get_main_queue(), ^{ 464 EX_ENSURE_STRONGIFY(self); 465 [self.appLoadingCancelView removeFromSuperview]; 466 self.appLoadingCancelView = nil; 467 }); 468 } 469 [self _showOrReconfigureManagedAppSplashScreen:manifest]; 470 [self _setLoadingViewStatusIfEnabledFromAppLoader:appLoader]; 471 if ([EXKernel sharedInstance].browserController) { 472 [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest]; 473 } 474 [self _rebuildBridge]; 475} 476 477- (void)appLoader:(EXAbstractLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress 478{ 479 if (self->_appRecord.appManager.status != kEXReactAppManagerStatusRunning) { 480 [self.appLoadingProgressWindowController updateStatusWithProgress:progress]; 481 } 482} 483 484- (void)appLoader:(EXAbstractLoader *)appLoader didFinishLoadingManifest:(EXManifestsManifest *)manifest bundle:(NSData *)data 485{ 486 [self _showOrReconfigureManagedAppSplashScreen:manifest]; 487 if (!_isHomeApp) { 488 [EXTextDirectionController setSupportsRTL:[self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest]]; 489 } 490 [self _rebuildBridge]; 491 if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 492 [self->_appRecord.appManager appLoaderFinished]; 493 } 494 495 if (!appLoader.isUpToDate && appLoader.shouldShowRemoteUpdateStatus) { 496 [self _showCachedExperienceAlert]; 497 } 498} 499 500- (void)appLoader:(EXAbstractLoader *)appLoader didFailWithError:(NSError *)error 501{ 502 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 503 [_appRecord.appManager appLoaderFailedWithError:error]; 504 } 505 [self maybeShowError:error]; 506} 507 508- (void)appLoader:(EXAbstractLoader *)appLoader didResolveUpdatedBundleWithManifest:(EXManifestsManifest * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error 509{ 510 [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error]; 511} 512 513#pragma mark - EXReactAppManagerDelegate 514 515- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager 516{ 517 UIView *reactView = appManager.rootView; 518 reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 519 reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 520 // Set this view to transparent so the root view background color aligns with custom development clients where the 521 // background color is the view controller root view. 522 reactView.backgroundColor = [UIColor clearColor]; 523 524 [_contentView removeFromSuperview]; 525 _contentView = reactView; 526 [self.view addSubview:_contentView]; 527 [self.view sendSubviewToBack:_contentView]; 528 [reactView becomeFirstResponder]; 529 530 // Set root view background color after adding as subview so we can access window 531 [self _setBackgroundColor]; 532} 533 534- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager 535{ 536 EXAssertMainThread(); 537 self.isLoading = YES; 538} 539 540- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager 541{ 542 EXAssertMainThread(); 543 self.isLoading = NO; 544 if ([EXKernel sharedInstance].browserController) { 545 [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord]; 546 } 547} 548 549- (void)reactAppManagerAppContentDidAppear:(EXReactAppManager *)appManager 550{ 551 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 552 [splashScreenService onAppContentDidAppear:self]; 553} 554 555- (void)reactAppManagerAppContentWillReload:(EXReactAppManager *)appManager { 556 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 557 [splashScreenService onAppContentWillReload:self]; 558} 559 560- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error 561{ 562 EXAssertMainThread(); 563 [self maybeShowError:error]; 564} 565 566- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager 567{ 568} 569 570- (void)errorViewDidSelectRetry:(EXErrorView *)errorView 571{ 572 [self refresh]; 573} 574 575#pragma mark - orientation 576 577- (UIInterfaceOrientationMask)supportedInterfaceOrientations 578{ 579 if ([self shouldUseRNScreenOrientation]) { 580 return [super supportedInterfaceOrientations]; 581 } 582 583#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 584 EXScreenOrientationRegistry *screenOrientationRegistry = (EXScreenOrientationRegistry *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 585 if (screenOrientationRegistry && [screenOrientationRegistry requiredOrientationMask] > 0) { 586 return [screenOrientationRegistry requiredOrientationMask]; 587 } 588#endif 589 590 return [self orientationMaskFromManifestOrDefault]; 591} 592 593- (BOOL)shouldUseRNScreenOrientation 594{ 595 Class screenWindowTraitsClass = [self->_appRecord.appManager versionedClassFromString:@"RNSScreenWindowTraits"]; 596 if ([screenWindowTraitsClass respondsToSelector:@selector(shouldAskScreensForScreenOrientationInViewController:)]) { 597 id<EXKernelRNSScreenWindowTraits> screenWindowTraits = (id<EXKernelRNSScreenWindowTraits>)screenWindowTraitsClass; 598 return [screenWindowTraits shouldAskScreensForScreenOrientationInViewController:self]; 599 } 600 return NO; 601} 602 603- (UIInterfaceOrientationMask)orientationMaskFromManifestOrDefault { 604 if (_appRecord.appLoader.manifest) { 605 NSString *orientationConfig = _appRecord.appLoader.manifest.orientation; 606 if ([orientationConfig isEqualToString:@"portrait"]) { 607 // lock to portrait 608 return UIInterfaceOrientationMaskPortrait; 609 } else if ([orientationConfig isEqualToString:@"landscape"]) { 610 // lock to landscape 611 return UIInterfaceOrientationMaskLandscape; 612 } 613 } 614 // no config or default value: allow autorotation 615 return UIInterfaceOrientationMaskAllButUpsideDown; 616} 617 618- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection { 619 [super traitCollectionDidChange:previousTraitCollection]; 620 if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass) 621 || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) { 622 623 #if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 624 EXScreenOrientationRegistry *screenOrientationRegistryController = (EXScreenOrientationRegistry *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 625 [screenOrientationRegistryController traitCollectionDidChangeTo:self.traitCollection]; 626 #endif 627 } 628} 629 630#pragma mark - RCTAppearanceModule 631 632/** 633 * This function overrides behaviour of RCTAppearanceModule 634 * basing on 'userInterfaceStyle' option from the app manifest. 635 * It also defaults the RCTAppearanceModule to 'light'. 636 */ 637- (void)_overrideAppearanceModuleBehaviour 638{ 639 NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest]; 640 NSString *appearancePreference = nil; 641 if ([userInterfaceStyle isEqualToString:@"light"]) { 642 appearancePreference = @"light"; 643 } else if ([userInterfaceStyle isEqualToString:@"dark"]) { 644 appearancePreference = @"dark"; 645 } else if ([userInterfaceStyle isEqualToString:@"automatic"]) { 646 appearancePreference = nil; 647 } 648 RCTOverrideAppearancePreference(appearancePreference); 649#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI48_0_0React/ABI48_0_0RCTAppearance.h>) 650 ABI48_0_0RCTOverrideAppearancePreference(appearancePreference); 651#endif 652#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI47_0_0React/ABI47_0_0RCTAppearance.h>) 653 ABI47_0_0RCTOverrideAppearancePreference(appearancePreference); 654#endif 655#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI46_0_0React/ABI46_0_0RCTAppearance.h>) 656 ABI46_0_0RCTOverrideAppearancePreference(appearancePreference); 657#endif 658 659} 660 661#pragma mark - user interface style 662 663- (void)_overrideUserInterfaceStyleOf:(UIViewController *)viewController 664{ 665 if (@available(iOS 13.0, *)) { 666 NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest]; 667 viewController.overrideUserInterfaceStyle = [self _userInterfaceStyleForString:userInterfaceStyle]; 668 } 669} 670 671- (NSString * _Nullable)_readUserInterfaceStyleFromManifest:(EXManifestsManifest *)manifest 672{ 673 return manifest.userInterfaceStyle; 674} 675 676- (UIUserInterfaceStyle)_userInterfaceStyleForString:(NSString *)userInterfaceStyleString API_AVAILABLE(ios(12.0)) { 677 if ([userInterfaceStyleString isEqualToString:@"dark"]) { 678 return UIUserInterfaceStyleDark; 679 } 680 if ([userInterfaceStyleString isEqualToString:@"automatic"]) { 681 return UIUserInterfaceStyleUnspecified; 682 } 683 if ([userInterfaceStyleString isEqualToString:@"light"]) { 684 return UIUserInterfaceStyleLight; 685 } 686 687 return UIUserInterfaceStyleUnspecified; 688} 689 690#pragma mark - root view and window background color 691 692- (void)_setBackgroundColor 693{ 694 NSString *backgroundColorString = [self _readBackgroundColorFromManifest:_appRecord.appLoader.manifest]; 695 UIColor *backgroundColor = [EXUtil colorWithHexString:backgroundColorString]; 696 self.view.backgroundColor = [UIColor clearColor]; 697 698 // NOTE(evanbacon): `self.view.window.rootViewController.view` represents the top-most window's root view controller's view which is the same 699 // view we set in `expo-system-ui`'s `setBackgroundColorAsync` method. 700 if (backgroundColor) { 701 if (self.view.window.rootViewController != nil && self.view.window.rootViewController.view != nil) { 702 self.view.window.rootViewController.view.backgroundColor = backgroundColor; 703 } 704 self.view.window.backgroundColor = backgroundColor; 705 } else { 706 // Reset this color to white so splash and other screens don't load against a black background. 707 if (self.view.window.rootViewController != nil && self.view.window.rootViewController.view != nil) { 708 self.view.window.rootViewController.view.backgroundColor = [UIColor whiteColor]; 709 } 710 // NOTE(brentvatne): we used to use white as a default background color for window but this caused 711 // problems when using form sheet presentation style with vcs eg: <Modal /> and native-stack. Most 712 // users expect the background behind these to be black, which is the default if backgroundColor is nil. 713 self.view.window.backgroundColor = nil; 714 715 // NOTE(brentvatne): we may want to default to respecting the default system background color 716 // on iOS13 and higher, but if we do make this choice then we will have to implement it on Android 717 // as well. This would also be a breaking change. Leaving this here as a placeholder for the future. 718 // if (@available(iOS 13.0, *)) { 719 // self.view.backgroundColor = [UIColor systemBackgroundColor]; 720 // } else { 721 // self.view.backgroundColor = [UIColor whiteColor]; 722 // } 723 } 724} 725 726- (NSString * _Nullable)_readBackgroundColorFromManifest:(EXManifestsManifest *)manifest 727{ 728 return manifest.iosOrRootBackgroundColor; 729} 730 731 732#pragma mark - Internal 733 734- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error 735{ 736 EXAssertMainThread(); 737 _dtmLastFatalErrorShown = [NSDate date]; 738 if (_errorView && _contentView == _errorView) { 739 // already showing, just update 740 _errorView.type = type; 741 _errorView.error = error; 742 } { 743 [_contentView removeFromSuperview]; 744 if (!_errorView) { 745 _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)]; 746 _errorView.delegate = self; 747 _errorView.appRecord = _appRecord; 748 } 749 _errorView.type = type; 750 _errorView.error = error; 751 _contentView = _errorView; 752 [self.view addSubview:_contentView]; 753 [[EXAnalytics sharedInstance] logErrorVisibleEvent]; 754 } 755} 756 757- (void)setIsLoading:(BOOL)isLoading 758{ 759 if ([_appRecord.appManager enablesDeveloperTools] && _dtmLastFatalErrorShown) { 760 if ([_dtmLastFatalErrorShown timeIntervalSinceNow] >= -kEXDevelopmentErrorCoolDownSeconds) { 761 // we just showed a fatal error very recently, do not begin loading. 762 // this can happen in some cases where react native sends the 'started loading' notif 763 // in spite of a packager error. 764 return; 765 } 766 } 767 _isLoading = isLoading; 768 EX_WEAKIFY(self); 769 dispatch_async(dispatch_get_main_queue(), ^{ 770 EX_ENSURE_STRONGIFY(self); 771 if (!isLoading) { 772 [self.appLoadingProgressWindowController hide]; 773 } 774 }); 775} 776 777#pragma mark - error recovery 778 779- (BOOL)_willAutoRecoverFromError:(NSError *)error 780{ 781 if (![_appRecord.appManager enablesDeveloperTools]) { 782 BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceShouldReloadOnError:_appRecord.scopeKey]; 783 if (shouldRecover) { 784 [self _invalidateRecoveryTimer]; 785 _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds 786 target:self 787 selector:@selector(refresh) 788 userInfo:nil 789 repeats:NO]; 790 } 791 return shouldRecover; 792 } 793 return NO; 794} 795 796- (void)_invalidateRecoveryTimer 797{ 798 if (_tmrAutoReloadDebounce) { 799 [_tmrAutoReloadDebounce invalidate]; 800 _tmrAutoReloadDebounce = nil; 801 } 802} 803 804#pragma mark - EXAppLoadingCancelViewDelegate 805 806- (void)appLoadingCancelViewDidCancel:(EXAppLoadingCancelView *)view { 807 if ([EXKernel sharedInstance].browserController) { 808 [[EXKernel sharedInstance].browserController moveHomeToVisible]; 809 } 810} 811 812@end 813 814NS_ASSUME_NONNULL_END 815