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