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