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 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 Client 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@synthesize supportedInterfaceOrientations = _supportedInterfaceOrientations; 92 93#pragma mark - Lifecycle 94 95- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record 96{ 97 if (self = [super init]) { 98 _appRecord = record; 99 _supportedInterfaceOrientations = EX_INTERFACE_ORIENTATION_USE_MANIFEST; 100 _isStandalone = [EXEnvironment sharedEnvironment].isDetached; 101 } 102 return self; 103} 104 105- (void)dealloc 106{ 107 [self _invalidateRecoveryTimer]; 108 [[NSNotificationCenter defaultCenter] removeObserver:self]; 109} 110 111- (void)viewDidLoad 112{ 113 [super viewDidLoad]; 114 115 // EXKernel.appRegistry.homeAppRecord does not contain any homeAppRecord until this point, 116 // therefore we cannot move this property initialization to the constructor/initializer 117 _isHomeApp = _appRecord == [EXKernel sharedInstance].appRegistry.homeAppRecord; 118 119 // show LoadingCancelView in managed apps only 120 if (!self.isStandalone && !self.isHomeApp) { 121 self.appLoadingCancelView = [EXAppLoadingCancelView new]; 122 // if home app is available then LoadingCancelView can show `go to home` button 123 if ([EXKernel sharedInstance].appRegistry.homeAppRecord) { 124 self.appLoadingCancelView.delegate = self; 125 } 126 [self.view addSubview:self.appLoadingCancelView]; 127 [self.view bringSubviewToFront:self.appLoadingCancelView]; 128 } 129 130 // show LoadingProgressWindow in managed apps and dev home app only 131 BOOL isDevelopmentHomeApp = self.isHomeApp && [EXEnvironment sharedEnvironment].isDebugXCodeScheme; 132 self.appLoadingProgressWindowController = [[EXAppLoadingProgressWindowController alloc] initWithEnabled:!self.isStandalone || isDevelopmentHomeApp]; 133 134 // show SplashScreen in standalone apps and home app only 135 // SplashScreen for managed is shown once the manifest is available 136 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 137 if (self.isHomeApp) { 138 EXHomeAppSplashScreenViewProvider *homeAppSplashScreenViewProvider = [EXHomeAppSplashScreenViewProvider new]; 139 [splashScreenService showSplashScreenFor:self 140 splashScreenViewProvider:homeAppSplashScreenViewProvider 141 successCallback:^{} 142 failureCallback:^(NSString *message){ UMLogWarn(@"%@", message); }]; 143 } else if (self.isStandalone) { 144 [splashScreenService showSplashScreenFor:self]; 145 } 146 147 self.view.backgroundColor = [UIColor whiteColor]; 148 _appRecord.appManager.delegate = self; 149 self.isLoading = YES; 150} 151 152- (void)viewDidAppear:(BOOL)animated 153{ 154 [super viewDidAppear:animated]; 155 if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) { 156 _appRecord.appLoader.delegate = self; 157 _appRecord.appLoader.dataSource = _appRecord.appManager; 158 [self refresh]; 159 } 160} 161 162- (BOOL)shouldAutorotate 163{ 164 return YES; 165} 166 167- (void)viewWillLayoutSubviews 168{ 169 [super viewWillLayoutSubviews]; 170 if (_appLoadingCancelView) { 171 _appLoadingCancelView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 172 } 173 if (_contentView) { 174 _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 175 } 176} 177 178- (void)viewWillDisappear:(BOOL)animated 179{ 180 [_appLoadingProgressWindowController hide]; 181 [super viewWillDisappear:animated]; 182} 183 184/** 185 * Force presented view controllers to use the same user interface style. 186 */ 187- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion 188{ 189 [super presentViewController:viewControllerToPresent animated:flag completion:completion]; 190 [self _overrideUserInterfaceStyleOf:viewControllerToPresent]; 191} 192 193/** 194 * Force child view controllers to use the same user interface style. 195 */ 196- (void)addChildViewController:(UIViewController *)childController 197{ 198 [super addChildViewController:childController]; 199 [self _overrideUserInterfaceStyleOf:childController]; 200} 201 202#pragma mark - Public 203 204- (void)maybeShowError:(NSError *)error 205{ 206 self.isLoading = NO; 207 if ([self _willAutoRecoverFromError:error]) { 208 return; 209 } 210 if (error && ![error isKindOfClass:[NSError class]]) { 211#if DEBUG 212 NSAssert(NO, @"AppViewController error handler was called on an object that isn't an NSError"); 213#endif 214 return; 215 } 216 217 // we don't ever want to show any Expo UI in a production standalone app, so hard crash 218 if ([EXEnvironment sharedEnvironment].isDetached && ![_appRecord.appManager enablesDeveloperTools]) { 219 NSException *e = [NSException exceptionWithName:@"ExpoFatalError" 220 reason:[NSString stringWithFormat:@"Expo encountered a fatal error: %@", [error localizedDescription]] 221 userInfo:@{NSUnderlyingErrorKey: error}]; 222 @throw e; 223 } 224 225 NSString *domain = (error && error.domain) ? error.domain : @""; 226 BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:EXNetworkErrorDomain]); 227 228 if (isNetworkError) { 229 // show a human-readable reachability error 230 dispatch_async(dispatch_get_main_queue(), ^{ 231 [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; 232 }); 233 } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { 234 // RCTRedBox already handled this 235 } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { 236 // RCTRedBox already handled this 237 } else { 238 dispatch_async(dispatch_get_main_queue(), ^{ 239 [self _showErrorWithType:kEXFatalErrorTypeException error:error]; 240 }); 241 } 242} 243 244- (void)refresh 245{ 246 self.isLoading = YES; 247 self.isBridgeAlreadyLoading = NO; 248 [self _invalidateRecoveryTimer]; 249 [_appRecord.appLoader request]; 250} 251 252- (void)reloadFromCache 253{ 254 self.isLoading = YES; 255 self.isBridgeAlreadyLoading = NO; 256 [self _invalidateRecoveryTimer]; 257 [_appRecord.appLoader requestFromCache]; 258} 259 260- (void)appStateDidBecomeActive 261{ 262 dispatch_async(dispatch_get_main_queue(), ^{ 263 [self _enforceDesiredDeviceOrientation]; 264 265 // Reset the root view background color and window color if we switch between Expo home and project 266 [self _setBackgroundColor:self.view]; 267 }); 268 [_appRecord.appManager appStateDidBecomeActive]; 269} 270 271- (void)appStateDidBecomeInactive 272{ 273 [_appRecord.appManager appStateDidBecomeInactive]; 274} 275 276- (void)_rebuildBridge 277{ 278 if (!self.isBridgeAlreadyLoading) { 279 self.isBridgeAlreadyLoading = YES; 280 dispatch_async(dispatch_get_main_queue(), ^{ 281 [self _overrideUserInterfaceStyleOf:self]; 282 [self _enforceDesiredDeviceOrientation]; 283 [self _invalidateRecoveryTimer]; 284 [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:self.appRecord]; 285 [self.appRecord.appManager rebuildBridge]; 286 }); 287 } 288} 289 290- (void)foregroundControllers 291{ 292 if (_backgroundedControllers != nil) { 293 __block UIViewController *parentController = self; 294 295 [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) { 296 [parentController presentViewController:viewController animated:NO completion:nil]; 297 parentController = viewController; 298 }]; 299 300 _backgroundedControllers = nil; 301 } 302} 303 304- (void)backgroundControllers 305{ 306 UIViewController *childController = [self presentedViewController]; 307 308 if (childController != nil) { 309 if (_backgroundedControllers == nil) { 310 _backgroundedControllers = [NSMutableArray new]; 311 } 312 313 while (childController != nil) { 314 [_backgroundedControllers addObject:childController]; 315 childController = childController.presentedViewController; 316 } 317 } 318} 319 320/** 321 * In managed app we expect two kinds of manifest: 322 * - optimistic one (served from cache) 323 * - actual one served when app is fetched. 324 * For each of them we should show SplashScreen, 325 * therefore for any consecutive SplashScreen.show call we just reconfigure what's already visible. 326 * In HomeApp or standalone apps this function is no-op as SplashScreen is managed differently. 327 */ 328- (void)_showOrReconfigureManagedAppSplashScreen:(NSDictionary *)manifest 329{ 330 if (_isStandalone || _isHomeApp) { 331 return; 332 } 333 if (!_managedAppSplashScreenViewProvider) { 334 _managedAppSplashScreenViewProvider = [[EXManagedAppSplashScreenViewProvider alloc] initWithManifest:manifest]; 335 336 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 337 UM_WEAKIFY(self); 338 dispatch_async(dispatch_get_main_queue(), ^{ 339 UM_ENSURE_STRONGIFY(self); 340 [splashScreenService showSplashScreenFor:self 341 splashScreenViewProvider:self.managedAppSplashScreenViewProvider 342 successCallback:^{} 343 failureCallback:^(NSString *message){ UMLogWarn(@"%@", message); }]; 344 }); 345 } else { 346 [_managedAppSplashScreenViewProvider updateSplashScreenViewWithManifest:manifest]; 347 } 348} 349 350- (void)_showCachedExperienceAlert 351{ 352 if (self.isStandalone || self.isHomeApp) { 353 return; 354 } 355 356 dispatch_async(dispatch_get_main_queue(), ^{ 357 UIAlertController *alert = [UIAlertController 358 alertControllerWithTitle:@"Using a cached project" 359 message:@"If you did not intend to use a cached project, check your network connection and reload." 360 preferredStyle:UIAlertControllerStyleAlert]; 361 [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; 362 [self presentViewController:alert animated:YES completion:nil]; 363 }); 364} 365 366#pragma mark - EXAppLoaderDelegate 367 368- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest 369{ 370 if (_appLoadingCancelView) { 371 UM_WEAKIFY(self); 372 dispatch_async(dispatch_get_main_queue(), ^{ 373 UM_ENSURE_STRONGIFY(self); 374 [self.appLoadingCancelView removeFromSuperview]; 375 self.appLoadingCancelView = nil; 376 }); 377 } 378 [self _showOrReconfigureManagedAppSplashScreen:manifest]; 379 if ([EXKernel sharedInstance].browserController) { 380 [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest]; 381 } 382 [self _rebuildBridge]; 383} 384 385- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress 386{ 387 [self.appLoadingProgressWindowController updateStatusWithProgress:progress]; 388} 389 390- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data 391{ 392 [self _showOrReconfigureManagedAppSplashScreen:manifest]; 393 [self _rebuildBridge]; 394 if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 395 [self->_appRecord.appManager appLoaderFinished]; 396 } 397 398 if (!appLoader.isUpToDate) { 399 [self _showCachedExperienceAlert]; 400 } 401} 402 403- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error 404{ 405 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 406 [_appRecord.appManager appLoaderFailedWithError:error]; 407 } 408 [self maybeShowError:error]; 409} 410 411- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(NSDictionary * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error 412{ 413 [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error]; 414} 415 416#pragma mark - EXReactAppManagerDelegate 417 418- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager 419{ 420 UIView *reactView = appManager.rootView; 421 reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 422 reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 423 424 425 [_contentView removeFromSuperview]; 426 _contentView = reactView; 427 [self.view addSubview:_contentView]; 428 [self.view sendSubviewToBack:_contentView]; 429 [reactView becomeFirstResponder]; 430 431 // Set root view background color after adding as subview so we can access window 432 [self _setBackgroundColor:reactView]; 433} 434 435- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager 436{ 437 EXAssertMainThread(); 438 self.isLoading = YES; 439} 440 441- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager 442{ 443 EXAssertMainThread(); 444 self.isLoading = NO; 445 if ([EXKernel sharedInstance].browserController) { 446 [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord]; 447 } 448} 449 450- (void)reactAppManagerAppContentDidAppear:(EXReactAppManager *)appManager 451{ 452 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 453 [splashScreenService onAppContentDidAppear:self]; 454} 455 456- (void)reactAppManagerAppContentWillReload:(EXReactAppManager *)appManager { 457 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 458 [splashScreenService onAppContentWillReload:self]; 459} 460 461- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error 462{ 463 EXAssertMainThread(); 464 [self maybeShowError:error]; 465} 466 467- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager 468{ 469#if __has_include(<EXGL_CPP/UEXGL.h>) 470 UEXGLInvalidateJsiCache(); 471#endif 472} 473 474- (void)errorViewDidSelectRetry:(EXErrorView *)errorView 475{ 476 [self refresh]; 477} 478 479#pragma mark - orientation 480 481- (UIInterfaceOrientationMask)supportedInterfaceOrientations 482{ 483#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 484 EXScreenOrientationRegistry *screenOrientationRegistry = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 485 if (screenOrientationRegistry && [screenOrientationRegistry requiredOrientationMask] > 0) { 486 return [screenOrientationRegistry requiredOrientationMask]; 487 } 488#endif 489 490 // TODO: Remove once sdk 37 is phased out 491 if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) { 492 return _supportedInterfaceOrientations; 493 } 494 495 return [self orientationMaskFromManifestOrDefault]; 496} 497 498- (UIInterfaceOrientationMask)orientationMaskFromManifestOrDefault { 499 if (_appRecord.appLoader.manifest) { 500 NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"]; 501 if ([orientationConfig isEqualToString:@"portrait"]) { 502 // lock to portrait 503 return UIInterfaceOrientationMaskPortrait; 504 } else if ([orientationConfig isEqualToString:@"landscape"]) { 505 // lock to landscape 506 return UIInterfaceOrientationMaskLandscape; 507 } 508 } 509 // no config or default value: allow autorotation 510 return UIInterfaceOrientationMaskAllButUpsideDown; 511} 512 513// TODO: Remove once sdk 37 is phased out 514- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations 515{ 516 _supportedInterfaceOrientations = supportedInterfaceOrientations; 517 [self _enforceDesiredDeviceOrientation]; 518} 519 520- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection { 521 [super traitCollectionDidChange:previousTraitCollection]; 522 if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass) 523 || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) { 524 525 #if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 526 EXScreenOrientationRegistry *screenOrientationRegistryController = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 527 [screenOrientationRegistryController traitCollectionDidChangeTo:self.traitCollection]; 528 #endif 529 530 // TODO: Remove once sdk 37 is phased out 531 [[EXKernel sharedInstance].serviceRegistry.screenOrientationManager handleScreenOrientationChange:self.traitCollection]; 532 } 533} 534 535// TODO: Remove once sdk 37 is phased out 536- (void)_enforceDesiredDeviceOrientation 537{ 538 RCTAssertMainQueue(); 539 UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations]; 540 UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation]; 541 UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown; 542 switch (mask) { 543 case UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown: 544 if (!UIDeviceOrientationIsPortrait(currentOrientation)) { 545 newOrientation = UIInterfaceOrientationPortrait; 546 } 547 break; 548 case UIInterfaceOrientationMaskPortrait: 549 newOrientation = UIInterfaceOrientationPortrait; 550 break; 551 case UIInterfaceOrientationMaskPortraitUpsideDown: 552 newOrientation = UIInterfaceOrientationPortraitUpsideDown; 553 break; 554 case UIInterfaceOrientationMaskLandscape: 555 if (!UIDeviceOrientationIsLandscape(currentOrientation)) { 556 newOrientation = UIInterfaceOrientationLandscapeLeft; 557 } 558 break; 559 case UIInterfaceOrientationMaskLandscapeLeft: 560 newOrientation = UIInterfaceOrientationLandscapeLeft; 561 break; 562 case UIInterfaceOrientationMaskLandscapeRight: 563 newOrientation = UIInterfaceOrientationLandscapeRight; 564 break; 565 case UIInterfaceOrientationMaskAllButUpsideDown: 566 if (currentOrientation == UIDeviceOrientationFaceDown) { 567 newOrientation = UIInterfaceOrientationPortrait; 568 } 569 break; 570 default: 571 break; 572 } 573 if (newOrientation != UIInterfaceOrientationUnknown) { 574 [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"]; 575 } 576 [UIViewController attemptRotationToDeviceOrientation]; 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