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