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