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