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