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