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