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