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