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