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