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