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