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