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 self.view.backgroundColor = [UIColor whiteColor]; 153 _appRecord.appManager.delegate = self; 154 self.isLoading = YES; 155} 156 157- (void)viewDidAppear:(BOOL)animated 158{ 159 [super viewDidAppear:animated]; 160 if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) { 161 _appRecord.appLoader.delegate = self; 162 _appRecord.appLoader.dataSource = _appRecord.appManager; 163 [self refresh]; 164 } 165} 166 167- (BOOL)shouldAutorotate 168{ 169 return YES; 170} 171 172- (void)viewWillLayoutSubviews 173{ 174 [super viewWillLayoutSubviews]; 175 if (_appLoadingCancelView) { 176 _appLoadingCancelView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 177 } 178 if (_contentView) { 179 _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 180 } 181} 182 183- (void)viewWillDisappear:(BOOL)animated 184{ 185 [_appLoadingProgressWindowController hide]; 186 [super viewWillDisappear:animated]; 187} 188 189/** 190 * Force presented view controllers to use the same user interface style. 191 */ 192- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion 193{ 194 [super presentViewController:viewControllerToPresent animated:flag completion:completion]; 195 [self _overrideUserInterfaceStyleOf:viewControllerToPresent]; 196} 197 198/** 199 * Force child view controllers to use the same user interface style. 200 */ 201- (void)addChildViewController:(UIViewController *)childController 202{ 203 [super addChildViewController:childController]; 204 [self _overrideUserInterfaceStyleOf:childController]; 205} 206 207#pragma mark - Public 208 209- (void)maybeShowError:(NSError *)error 210{ 211 self.isLoading = NO; 212 if ([self _willAutoRecoverFromError:error]) { 213 return; 214 } 215 if (error && ![error isKindOfClass:[NSError class]]) { 216#if DEBUG 217 NSAssert(NO, @"AppViewController error handler was called on an object that isn't an NSError"); 218#endif 219 return; 220 } 221 222 // we don't ever want to show any Expo UI in a production standalone app, so hard crash 223 if ([EXEnvironment sharedEnvironment].isDetached && ![_appRecord.appManager enablesDeveloperTools]) { 224 NSException *e = [NSException exceptionWithName:@"ExpoFatalError" 225 reason:[NSString stringWithFormat:@"Expo encountered a fatal error: %@", [error localizedDescription]] 226 userInfo:@{NSUnderlyingErrorKey: error}]; 227 @throw e; 228 } 229 230 NSString *domain = (error && error.domain) ? error.domain : @""; 231 BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:EXNetworkErrorDomain]); 232 233 if (isNetworkError) { 234 // show a human-readable reachability error 235 dispatch_async(dispatch_get_main_queue(), ^{ 236 [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; 237 }); 238 } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { 239 // RCTRedBox already handled this 240 } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { 241 // RCTRedBox already handled this 242 } else { 243 dispatch_async(dispatch_get_main_queue(), ^{ 244 [self _showErrorWithType:kEXFatalErrorTypeException error:error]; 245 }); 246 } 247} 248 249- (void)refresh 250{ 251 self.isLoading = YES; 252 self.isBridgeAlreadyLoading = NO; 253 [self _invalidateRecoveryTimer]; 254 [_appRecord.appLoader request]; 255} 256 257- (void)reloadFromCache 258{ 259 self.isLoading = YES; 260 self.isBridgeAlreadyLoading = NO; 261 [self _invalidateRecoveryTimer]; 262 [_appRecord.appLoader requestFromCache]; 263} 264 265- (void)appStateDidBecomeActive 266{ 267 dispatch_async(dispatch_get_main_queue(), ^{ 268 [self _enforceDesiredDeviceOrientation]; 269 270 // Reset the root view background color and window color if we switch between Expo home and project 271 [self _setBackgroundColor:self.view]; 272 }); 273 [_appRecord.appManager appStateDidBecomeActive]; 274} 275 276- (void)appStateDidBecomeInactive 277{ 278 [_appRecord.appManager appStateDidBecomeInactive]; 279} 280 281- (void)_rebuildBridge 282{ 283 if (!self.isBridgeAlreadyLoading) { 284 self.isBridgeAlreadyLoading = YES; 285 dispatch_async(dispatch_get_main_queue(), ^{ 286 [self _overrideUserInterfaceStyleOf:self]; 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 * Non-managed apps (HomeApp or standalones) this function is no-op as SplashScreen is managed differently. 332 */ 333- (void)_showOrReconfigureManagedAppSplashScreen:(NSDictionary *)manifest 334{ 335 if (_isStandalone || _isHomeApp) { 336 return; 337 } 338 if (!_managedAppSplashScreenViewProvider) { 339 _managedAppSplashScreenViewProvider = [[EXManagedAppSplashScreenViewProvider alloc] initWithManifest:manifest]; 340 341 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 342 [splashScreenService showSplashScreenFor:self 343 splashScreenViewProvider:_managedAppSplashScreenViewProvider 344 successCallback:^{} 345 failureCallback:^(NSString *message){ UMLogWarn(@"%@", message); }]; 346 } else { 347 [_managedAppSplashScreenViewProvider updateSplashScreenViewWithManifest:manifest]; 348 } 349} 350 351- (void)_showCachedExperienceAlert 352{ 353 if (self.isStandalone || self.isHomeApp) { 354 return; 355 } 356 357 dispatch_async(dispatch_get_main_queue(), ^{ 358 UIAlertController *alert = [UIAlertController 359 alertControllerWithTitle:@"Using a cached project" 360 message:@"If you did not intend to use a cached project, check your network connection and reload." 361 preferredStyle:UIAlertControllerStyleAlert]; 362 [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; 363 [self presentViewController:alert animated:YES completion:nil]; 364 }); 365} 366 367#pragma mark - EXAppLoaderDelegate 368 369- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest 370{ 371 if (_appLoadingCancelView) { 372 [_appLoadingCancelView removeFromSuperview]; 373 _appLoadingCancelView = nil; 374 } 375 [self _showOrReconfigureManagedAppSplashScreen:manifest]; 376 if ([EXKernel sharedInstance].browserController) { 377 [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest]; 378 } 379 [self _rebuildBridge]; 380} 381 382- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress 383{ 384 [self.appLoadingProgressWindowController updateStatusWithProgress:progress]; 385} 386 387- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data 388{ 389 [self _showOrReconfigureManagedAppSplashScreen:manifest]; 390 [self _rebuildBridge]; 391 if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 392 [self->_appRecord.appManager appLoaderFinished]; 393 } 394 395 if (!appLoader.isUpToDate) { 396 [self _showCachedExperienceAlert]; 397 } 398} 399 400- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error 401{ 402 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 403 [_appRecord.appManager appLoaderFailedWithError:error]; 404 } 405 [self maybeShowError:error]; 406} 407 408- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(NSDictionary * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error 409{ 410 [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error]; 411} 412 413#pragma mark - EXReactAppManagerDelegate 414 415- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager 416{ 417 UIView *reactView = appManager.rootView; 418 reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 419 reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 420 421 422 [_contentView removeFromSuperview]; 423 _contentView = reactView; 424 [self.view addSubview:_contentView]; 425 [self.view sendSubviewToBack:_contentView]; 426 [reactView becomeFirstResponder]; 427 428 // Set root view background color after adding as subview so we can access window 429 [self _setBackgroundColor:reactView]; 430} 431 432- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager 433{ 434 EXAssertMainThread(); 435 self.isLoading = YES; 436} 437 438- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager 439{ 440 EXAssertMainThread(); 441 self.isLoading = NO; 442 if ([EXKernel sharedInstance].browserController) { 443 [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord]; 444 } 445} 446 447- (void)reactAppManagerAppContentDidAppear:(EXReactAppManager *)appManager 448{ 449 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 450 [splashScreenService onAppContentDidAppear:self]; 451} 452 453- (void)reactAppManagerAppContentWillReload:(EXReactAppManager *)appManager { 454 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 455 [splashScreenService onAppContentWillReload:self]; 456} 457 458- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error 459{ 460 EXAssertMainThread(); 461 [self maybeShowError:error]; 462} 463 464- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager 465{ 466#if __has_include(<EXGL_CPP/UEXGL.h>) 467 UEXGLInvalidateJsiCache(); 468#endif 469} 470 471- (void)errorViewDidSelectRetry:(EXErrorView *)errorView 472{ 473 [self refresh]; 474} 475 476#pragma mark - orientation 477 478- (UIInterfaceOrientationMask)supportedInterfaceOrientations 479{ 480#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 481 EXScreenOrientationRegistry *screenOrientationRegistry = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 482 if (screenOrientationRegistry && [screenOrientationRegistry requiredOrientationMask] > 0) { 483 return [screenOrientationRegistry requiredOrientationMask]; 484 } 485#endif 486 487 // TODO: Remove once sdk 37 is phased out 488 if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) { 489 return _supportedInterfaceOrientations; 490 } 491 492 return [self orientationMaskFromManifestOrDefault]; 493} 494 495- (UIInterfaceOrientationMask)orientationMaskFromManifestOrDefault { 496 if (_appRecord.appLoader.manifest) { 497 NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"]; 498 if ([orientationConfig isEqualToString:@"portrait"]) { 499 // lock to portrait 500 return UIInterfaceOrientationMaskPortrait; 501 } else if ([orientationConfig isEqualToString:@"landscape"]) { 502 // lock to landscape 503 return UIInterfaceOrientationMaskLandscape; 504 } 505 } 506 // no config or default value: allow autorotation 507 return UIInterfaceOrientationMaskAllButUpsideDown; 508} 509 510// TODO: Remove once sdk 37 is phased out 511- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations 512{ 513 _supportedInterfaceOrientations = supportedInterfaceOrientations; 514 [self _enforceDesiredDeviceOrientation]; 515} 516 517- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection { 518 [super traitCollectionDidChange:previousTraitCollection]; 519 if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass) 520 || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) { 521 522 #if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 523 EXScreenOrientationRegistry *screenOrientationRegistryController = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 524 [screenOrientationRegistryController traitCollectionDidChangeTo:self.traitCollection]; 525 #endif 526 527 // TODO: Remove once sdk 37 is phased out 528 [[EXKernel sharedInstance].serviceRegistry.screenOrientationManager handleScreenOrientationChange:self.traitCollection]; 529 } 530} 531 532// TODO: Remove once sdk 37 is phased out 533- (void)_enforceDesiredDeviceOrientation 534{ 535 RCTAssertMainQueue(); 536 UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations]; 537 UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation]; 538 UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown; 539 switch (mask) { 540 case UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown: 541 if (!UIDeviceOrientationIsPortrait(currentOrientation)) { 542 newOrientation = UIInterfaceOrientationPortrait; 543 } 544 break; 545 case UIInterfaceOrientationMaskPortrait: 546 newOrientation = UIInterfaceOrientationPortrait; 547 break; 548 case UIInterfaceOrientationMaskPortraitUpsideDown: 549 newOrientation = UIInterfaceOrientationPortraitUpsideDown; 550 break; 551 case UIInterfaceOrientationMaskLandscape: 552 if (!UIDeviceOrientationIsLandscape(currentOrientation)) { 553 newOrientation = UIInterfaceOrientationLandscapeLeft; 554 } 555 break; 556 case UIInterfaceOrientationMaskLandscapeLeft: 557 newOrientation = UIInterfaceOrientationLandscapeLeft; 558 break; 559 case UIInterfaceOrientationMaskLandscapeRight: 560 newOrientation = UIInterfaceOrientationLandscapeRight; 561 break; 562 case UIInterfaceOrientationMaskAllButUpsideDown: 563 if (currentOrientation == UIDeviceOrientationFaceDown) { 564 newOrientation = UIInterfaceOrientationPortrait; 565 } 566 break; 567 default: 568 break; 569 } 570 if (newOrientation != UIInterfaceOrientationUnknown) { 571 [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"]; 572 } 573 [UIViewController attemptRotationToDeviceOrientation]; 574} 575 576#pragma mark - user interface style 577 578- (void)_overrideUserInterfaceStyleOf:(UIViewController *)viewController 579{ 580 if (@available(iOS 13.0, *)) { 581 NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest]; 582 viewController.overrideUserInterfaceStyle = [self _userInterfaceStyleForString:userInterfaceStyle]; 583 } 584} 585 586- (NSString * _Nullable)_readUserInterfaceStyleFromManifest:(NSDictionary *)manifest 587{ 588 if (manifest[@"ios"] && manifest[@"ios"][@"userInterfaceStyle"]) { 589 return manifest[@"ios"][@"userInterfaceStyle"]; 590 } 591 return manifest[@"userInterfaceStyle"]; 592} 593 594- (UIUserInterfaceStyle)_userInterfaceStyleForString:(NSString *)userInterfaceStyleString API_AVAILABLE(ios(12.0)) { 595 if ([userInterfaceStyleString isEqualToString:@"dark"]) { 596 return UIUserInterfaceStyleDark; 597 } 598 if ([userInterfaceStyleString isEqualToString:@"automatic"]) { 599 return UIUserInterfaceStyleUnspecified; 600 } 601 return UIUserInterfaceStyleLight; 602} 603 604#pragma mark - root view and window background color 605 606- (void)_setBackgroundColor:(UIView *)view 607{ 608 NSString *backgroundColorString = [self _readBackgroundColorFromManifest:_appRecord.appLoader.manifest]; 609 UIColor *backgroundColor = [EXUtil colorWithHexString:backgroundColorString]; 610 611 if (backgroundColor) { 612 view.backgroundColor = backgroundColor; 613 // NOTE(brentvatne): it may be desirable at some point to split the window backgroundColor out from the 614 // root view, we can do if use case is presented to us. 615 view.window.backgroundColor = backgroundColor; 616 } else { 617 view.backgroundColor = [UIColor whiteColor]; 618 619 // NOTE(brentvatne): we used to use white as a default background color for window but this caused 620 // problems when using form sheet presentation style with vcs eg: <Modal /> and native-stack. Most 621 // users expect the background behind these to be black, which is the default if backgroundColor is nil. 622 view.window.backgroundColor = nil; 623 624 // NOTE(brentvatne): we may want to default to respecting the default system background color 625 // on iOS13 and higher, but if we do make this choice then we will have to implement it on Android 626 // as well. This would also be a breaking change. Leaaving this here as a placeholder for the future. 627 // if (@available(iOS 13.0, *)) { 628 // view.backgroundColor = [UIColor systemBackgroundColor]; 629 // } else { 630 // view.backgroundColor = [UIColor whiteColor]; 631 // } 632 } 633} 634 635- (NSString * _Nullable)_readBackgroundColorFromManifest:(NSDictionary *)manifest 636{ 637 if (manifest[@"ios"] && manifest[@"ios"][@"backgroundColor"]) { 638 return manifest[@"ios"][@"backgroundColor"]; 639 } 640 return manifest[@"backgroundColor"]; 641} 642 643 644#pragma mark - Internal 645 646- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error 647{ 648 EXAssertMainThread(); 649 _dtmLastFatalErrorShown = [NSDate date]; 650 if (_errorView && _contentView == _errorView) { 651 // already showing, just update 652 _errorView.type = type; 653 _errorView.error = error; 654 } { 655 [_contentView removeFromSuperview]; 656 if (!_errorView) { 657 _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)]; 658 _errorView.delegate = self; 659 _errorView.appRecord = _appRecord; 660 } 661 _errorView.type = type; 662 _errorView.error = error; 663 _contentView = _errorView; 664 [self.view addSubview:_contentView]; 665 [[EXAnalytics sharedInstance] logErrorVisibleEvent]; 666 } 667} 668 669- (void)setIsLoading:(BOOL)isLoading 670{ 671 if ([_appRecord.appManager enablesDeveloperTools] && _dtmLastFatalErrorShown) { 672 if ([_dtmLastFatalErrorShown timeIntervalSinceNow] >= -kEXDevelopmentErrorCoolDownSeconds) { 673 // we just showed a fatal error very recently, do not begin loading. 674 // this can happen in some cases where react native sends the 'started loading' notif 675 // in spite of a packager error. 676 return; 677 } 678 } 679 _isLoading = isLoading; 680 UM_WEAKIFY(self); 681 dispatch_async(dispatch_get_main_queue(), ^{ 682 UM_ENSURE_STRONGIFY(self); 683 if (isLoading) { 684 685 } else { 686 [self.appLoadingProgressWindowController hide]; 687 } 688 }); 689} 690 691#pragma mark - error recovery 692 693- (BOOL)_willAutoRecoverFromError:(NSError *)error 694{ 695 if (![_appRecord.appManager enablesDeveloperTools]) { 696 BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdShouldReloadOnError:_appRecord.experienceId]; 697 if (shouldRecover) { 698 [self _invalidateRecoveryTimer]; 699 _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds 700 target:self 701 selector:@selector(refresh) 702 userInfo:nil 703 repeats:NO]; 704 } 705 return shouldRecover; 706 } 707 return NO; 708} 709 710- (void)_invalidateRecoveryTimer 711{ 712 if (_tmrAutoReloadDebounce) { 713 [_tmrAutoReloadDebounce invalidate]; 714 _tmrAutoReloadDebounce = nil; 715 } 716} 717 718#pragma mark - EXAppLoadingCancelViewDelegate 719 720- (void)appLoadingCancelViewDidCancel:(EXAppLoadingCancelView *)view { 721 if ([EXKernel sharedInstance].browserController) { 722 [[EXKernel sharedInstance].browserController moveHomeToVisible]; 723 } 724} 725 726@end 727 728NS_ASSUME_NONNULL_END 729