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