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