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