1// Copyright 2015-present 650 Industries. All rights reserved. 2 3@import UIKit; 4 5#import "EXAnalytics.h" 6#import "EXAppLoader.h" 7#import "EXAppLoadingView.h" 8#import "EXAppViewController.h" 9#import "EXEnvironment.h" 10#import "EXErrorRecoveryManager.h" 11#import "EXErrorView.h" 12#import "EXFileDownloader.h" 13#import "EXKernel.h" 14#import "EXKernelUtil.h" 15#import "EXReactAppManager.h" 16#import "EXScreenOrientationManager.h" 17#import "EXVersions.h" 18#import "EXUpdatesManager.h" 19#import "EXUtil.h" 20#import <UMCore/UMModuleRegistryProvider.h> 21 22#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 23#import <EXScreenOrientation/EXScreenOrientationRegistry.h> 24#endif 25 26#import <React/RCTUtils.h> 27 28#define EX_INTERFACE_ORIENTATION_USE_MANIFEST 0 29 30// when we encounter an error and auto-refresh, we may actually see a series of errors. 31// we only want to trigger refresh once, so we debounce refresh on a timer. 32const CGFloat kEXAutoReloadDebounceSeconds = 0.1; 33 34// in development only, some errors can happen before we even start loading 35// (e.g. certain packager errors, such as an invalid bundle url) 36// and we want to make sure not to cover the error with a loading view or other chrome. 37const CGFloat kEXDevelopmentErrorCoolDownSeconds = 0.1; 38 39NS_ASSUME_NONNULL_BEGIN 40 41@interface EXAppViewController () 42 <EXReactAppManagerUIDelegate, EXAppLoaderDelegate, EXErrorViewDelegate> 43 44@property (nonatomic, assign) BOOL isLoading; 45@property (nonatomic, assign) BOOL isBridgeAlreadyLoading; 46@property (nonatomic, weak) EXKernelAppRecord *appRecord; 47@property (nonatomic, strong) EXAppLoadingView *loadingView; 48@property (nonatomic, strong) EXErrorView *errorView; 49@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; // override super 50@property (nonatomic, strong) NSTimer *tmrAutoReloadDebounce; 51@property (nonatomic, strong) NSDate *dtmLastFatalErrorShown; 52@property (nonatomic, strong) NSMutableArray<UIViewController *> *backgroundedControllers; 53 54@end 55 56@implementation EXAppViewController 57 58@synthesize supportedInterfaceOrientations = _supportedInterfaceOrientations; 59 60#pragma mark - Lifecycle 61 62- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record 63{ 64 if (self = [super init]) { 65 _appRecord = record; 66 _supportedInterfaceOrientations = EX_INTERFACE_ORIENTATION_USE_MANIFEST; 67 } 68 return self; 69} 70 71- (void)dealloc 72{ 73 [self _invalidateRecoveryTimer]; 74 [[NSNotificationCenter defaultCenter] removeObserver:self]; 75} 76 77- (void)viewDidLoad 78{ 79 [super viewDidLoad]; 80 81 self.view.backgroundColor = [UIColor whiteColor]; 82 _loadingView = [[EXAppLoadingView alloc] initWithAppRecord:_appRecord]; 83 [self.view addSubview:_loadingView]; 84 _appRecord.appManager.delegate = self; 85 self.isLoading = YES; 86} 87 88- (void)viewDidAppear:(BOOL)animated 89{ 90 [super viewDidAppear:animated]; 91 if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) { 92 _appRecord.appLoader.delegate = self; 93 _appRecord.appLoader.dataSource = _appRecord.appManager; 94 [self refresh]; 95 } 96} 97 98- (BOOL)shouldAutorotate 99{ 100 return YES; 101} 102 103- (void)viewWillLayoutSubviews 104{ 105 [super viewWillLayoutSubviews]; 106 if (_loadingView) { 107 _loadingView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 108 } 109 if (_contentView) { 110 _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 111 } 112} 113 114/** 115 * Force presented view controllers to use the same user interface style. 116 */ 117- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion 118{ 119 [super presentViewController:viewControllerToPresent animated:flag completion:completion]; 120 [self _overrideUserInterfaceStyleOf:viewControllerToPresent]; 121} 122 123/** 124 * Force child view controllers to use the same user interface style. 125 */ 126- (void)addChildViewController:(UIViewController *)childController 127{ 128 [super addChildViewController:childController]; 129 [self _overrideUserInterfaceStyleOf:childController]; 130} 131 132#pragma mark - Public 133 134- (void)maybeShowError:(NSError *)error 135{ 136 self.isLoading = NO; 137 if ([self _willAutoRecoverFromError:error]) { 138 return; 139 } 140 if (error && ![error isKindOfClass:[NSError class]]) { 141#if DEBUG 142 NSAssert(NO, @"AppViewController error handler was called on an object that isn't an NSError"); 143#endif 144 return; 145 } 146 147 // we don't ever want to show any Expo UI in a production standalone app, so hard crash 148 if ([EXEnvironment sharedEnvironment].isDetached && ![_appRecord.appManager enablesDeveloperTools]) { 149 NSException *e = [NSException exceptionWithName:@"ExpoFatalError" 150 reason:[NSString stringWithFormat:@"Expo encountered a fatal error: %@", [error localizedDescription]] 151 userInfo:@{NSUnderlyingErrorKey: error}]; 152 @throw e; 153 } 154 155 NSString *domain = (error && error.domain) ? error.domain : @""; 156 BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:EXNetworkErrorDomain]); 157 158 if (isNetworkError) { 159 // show a human-readable reachability error 160 dispatch_async(dispatch_get_main_queue(), ^{ 161 [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; 162 }); 163 } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { 164 // RCTRedBox already handled this 165 } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { 166 // RCTRedBox already handled this 167 } else { 168 dispatch_async(dispatch_get_main_queue(), ^{ 169 [self _showErrorWithType:kEXFatalErrorTypeException error:error]; 170 }); 171 } 172} 173 174- (void)_rebuildBridge 175{ 176 [self _invalidateRecoveryTimer]; 177 [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:_appRecord]; 178 [_appRecord.appManager rebuildBridge]; 179} 180 181- (void)refresh 182{ 183 self.isLoading = YES; 184 self.isBridgeAlreadyLoading = NO; 185 [self _invalidateRecoveryTimer]; 186 [_appRecord.appLoader request]; 187} 188 189- (void)reloadFromCache 190{ 191 self.isLoading = YES; 192 self.isBridgeAlreadyLoading = NO; 193 [self _invalidateRecoveryTimer]; 194 [_appRecord.appLoader requestFromCache]; 195} 196 197- (void)appStateDidBecomeActive 198{ 199 dispatch_async(dispatch_get_main_queue(), ^{ 200 [self _enforceDesiredDeviceOrientation]; 201 202 // Reset the root view background color and window color if we switch between Expo home and project 203 [self _setBackgroundColor:self.view]; 204 }); 205 [_appRecord.appManager appStateDidBecomeActive]; 206} 207 208- (void)appStateDidBecomeInactive 209{ 210 [_appRecord.appManager appStateDidBecomeInactive]; 211} 212 213- (void)_rebuildBridgeWithLoadingViewManifest:(NSDictionary *)manifest 214{ 215 if (!self.isBridgeAlreadyLoading) { 216 self.isBridgeAlreadyLoading = YES; 217 dispatch_async(dispatch_get_main_queue(), ^{ 218 self->_loadingView.manifest = manifest; 219 [self _overrideUserInterfaceStyleOf:self]; 220 [self _enforceDesiredDeviceOrientation]; 221 [self _rebuildBridge]; 222 }); 223 } 224} 225 226- (void)foregroundControllers 227{ 228 if (_backgroundedControllers != nil) { 229 __block UIViewController *parentController = self; 230 231 [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) { 232 [parentController presentViewController:viewController animated:NO completion:nil]; 233 parentController = viewController; 234 }]; 235 236 _backgroundedControllers = nil; 237 } 238} 239 240- (void)backgroundControllers 241{ 242 UIViewController *childController = [self presentedViewController]; 243 244 if (childController != nil) { 245 if (_backgroundedControllers == nil) { 246 _backgroundedControllers = [NSMutableArray new]; 247 } 248 249 while (childController != nil) { 250 [_backgroundedControllers addObject:childController]; 251 childController = childController.presentedViewController; 252 } 253 } 254} 255 256#pragma mark - EXAppLoaderDelegate 257 258- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest 259{ 260 if ([EXKernel sharedInstance].browserController) { 261 [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest]; 262 } 263 [self _rebuildBridgeWithLoadingViewManifest:manifest]; 264} 265 266- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress 267{ 268 dispatch_async(dispatch_get_main_queue(), ^{ 269 [self->_loadingView updateStatusWithProgress:progress]; 270 }); 271} 272 273- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data 274{ 275 [self _rebuildBridgeWithLoadingViewManifest:manifest]; 276 if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 277 [self->_appRecord.appManager appLoaderFinished]; 278 } 279} 280 281- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error 282{ 283 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 284 [_appRecord.appManager appLoaderFailedWithError:error]; 285 } 286 [self maybeShowError:error]; 287} 288 289- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(NSDictionary * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error 290{ 291 [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error]; 292} 293 294#pragma mark - EXReactAppManagerDelegate 295 296- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager 297{ 298 UIView *reactView = appManager.rootView; 299 reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 300 reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 301 302 303 [_contentView removeFromSuperview]; 304 _contentView = reactView; 305 [self.view addSubview:_contentView]; 306 [self.view sendSubviewToBack:_contentView]; 307 [reactView becomeFirstResponder]; 308 309 // Set root view background color after adding as subview so we can access window 310 [self _setBackgroundColor:reactView]; 311} 312 313- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager 314{ 315 EXAssertMainThread(); 316 self.isLoading = YES; 317} 318 319- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager 320{ 321 EXAssertMainThread(); 322 self.isLoading = NO; 323 if ([EXKernel sharedInstance].browserController) { 324 [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord]; 325 } 326} 327 328- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error 329{ 330 EXAssertMainThread(); 331 [self maybeShowError:error]; 332} 333 334- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager 335{ 336 337} 338 339- (void)errorViewDidSelectRetry:(EXErrorView *)errorView 340{ 341 [self refresh]; 342} 343 344#pragma mark - orientation 345 346- (UIInterfaceOrientationMask)supportedInterfaceOrientations 347{ 348#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 349 EXScreenOrientationRegistry *screenOrientationRegistry = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 350 if (screenOrientationRegistry && [screenOrientationRegistry requiredOrientationMask] > 0) { 351 return [screenOrientationRegistry requiredOrientationMask]; 352 } 353#endif 354 355 // TODO: Remove once sdk 37 is phased out 356 if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) { 357 return _supportedInterfaceOrientations; 358 } 359 360 return [self orientationMaskFromManifestOrDefault]; 361} 362 363- (UIInterfaceOrientationMask)orientationMaskFromManifestOrDefault { 364 if (_appRecord.appLoader.manifest) { 365 NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"]; 366 if ([orientationConfig isEqualToString:@"portrait"]) { 367 // lock to portrait 368 return UIInterfaceOrientationMaskPortrait; 369 } else if ([orientationConfig isEqualToString:@"landscape"]) { 370 // lock to landscape 371 return UIInterfaceOrientationMaskLandscape; 372 } 373 } 374 // no config or default value: allow autorotation 375 return UIInterfaceOrientationMaskAllButUpsideDown; 376} 377 378// TODO: Remove once sdk 37 is phased out 379- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations 380{ 381 _supportedInterfaceOrientations = supportedInterfaceOrientations; 382 [self _enforceDesiredDeviceOrientation]; 383} 384 385- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection { 386 [super traitCollectionDidChange:previousTraitCollection]; 387 if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass) 388 || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) { 389 390 #if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 391 EXScreenOrientationRegistry *screenOrientationRegistryController = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 392 [screenOrientationRegistryController traitCollectionDidChangeTo:self.traitCollection]; 393 #endif 394 395 // TODO: Remove once sdk 37 is phased out 396 [[EXKernel sharedInstance].serviceRegistry.screenOrientationManager handleScreenOrientationChange:self.traitCollection]; 397 } 398} 399 400// TODO: Remove once sdk 37 is phased out 401- (void)_enforceDesiredDeviceOrientation 402{ 403 RCTAssertMainQueue(); 404 UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations]; 405 UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation]; 406 UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown; 407 switch (mask) { 408 case UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown: 409 if (!UIDeviceOrientationIsPortrait(currentOrientation)) { 410 newOrientation = UIInterfaceOrientationPortrait; 411 } 412 break; 413 case UIInterfaceOrientationMaskPortrait: 414 newOrientation = UIInterfaceOrientationPortrait; 415 break; 416 case UIInterfaceOrientationMaskPortraitUpsideDown: 417 newOrientation = UIInterfaceOrientationPortraitUpsideDown; 418 break; 419 case UIInterfaceOrientationMaskLandscape: 420 if (!UIDeviceOrientationIsLandscape(currentOrientation)) { 421 newOrientation = UIInterfaceOrientationLandscapeLeft; 422 } 423 break; 424 case UIInterfaceOrientationMaskLandscapeLeft: 425 newOrientation = UIInterfaceOrientationLandscapeLeft; 426 break; 427 case UIInterfaceOrientationMaskLandscapeRight: 428 newOrientation = UIInterfaceOrientationLandscapeRight; 429 break; 430 case UIInterfaceOrientationMaskAllButUpsideDown: 431 if (currentOrientation == UIDeviceOrientationFaceDown) { 432 newOrientation = UIInterfaceOrientationPortrait; 433 } 434 break; 435 default: 436 break; 437 } 438 if (newOrientation != UIInterfaceOrientationUnknown) { 439 [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"]; 440 } 441 [UIViewController attemptRotationToDeviceOrientation]; 442} 443 444#pragma mark - user interface style 445 446- (void)_overrideUserInterfaceStyleOf:(UIViewController *)viewController 447{ 448 if (@available(iOS 13.0, *)) { 449 NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest]; 450 viewController.overrideUserInterfaceStyle = [self _userInterfaceStyleForString:userInterfaceStyle]; 451 } 452} 453 454- (NSString * _Nullable)_readUserInterfaceStyleFromManifest:(NSDictionary *)manifest 455{ 456 if (manifest[@"ios"] && manifest[@"ios"][@"userInterfaceStyle"]) { 457 return manifest[@"ios"][@"userInterfaceStyle"]; 458 } 459 return manifest[@"userInterfaceStyle"]; 460} 461 462- (UIUserInterfaceStyle)_userInterfaceStyleForString:(NSString *)userInterfaceStyleString API_AVAILABLE(ios(12.0)) { 463 if ([userInterfaceStyleString isEqualToString:@"dark"]) { 464 return UIUserInterfaceStyleDark; 465 } 466 if ([userInterfaceStyleString isEqualToString:@"automatic"]) { 467 return UIUserInterfaceStyleUnspecified; 468 } 469 return UIUserInterfaceStyleLight; 470} 471 472#pragma mark - root view and window background color 473 474- (void)_setBackgroundColor:(UIView *)view 475{ 476 NSString *backgroundColorString = [self _readBackgroundColorFromManifest:_appRecord.appLoader.manifest]; 477 UIColor *backgroundColor = [EXUtil colorWithHexString:backgroundColorString]; 478 479 if (backgroundColor) { 480 view.backgroundColor = backgroundColor; 481 // NOTE(brentvatne): it may be desirable at some point to split the window backgroundColor out from the 482 // root view, we can do if use case is presented to us. 483 view.window.backgroundColor = backgroundColor; 484 } else { 485 view.backgroundColor = [UIColor whiteColor]; 486 487 // NOTE(brentvatne): we used to use white as a default background color for window but this caused 488 // problems when using form sheet presentation style with vcs eg: <Modal /> and native-stack. Most 489 // users expect the background behind these to be black, which is the default if backgroundColor is nil. 490 view.window.backgroundColor = nil; 491 492 // NOTE(brentvatne): we may want to default to respecting the default system background color 493 // on iOS13 and higher, but if we do make this choice then we will have to implement it on Android 494 // as well. This would also be a breaking change. Leaaving this here as a placeholder for the future. 495 // if (@available(iOS 13.0, *)) { 496 // view.backgroundColor = [UIColor systemBackgroundColor]; 497 // } else { 498 // view.backgroundColor = [UIColor whiteColor]; 499 // } 500 } 501} 502 503- (NSString * _Nullable)_readBackgroundColorFromManifest:(NSDictionary *)manifest 504{ 505 if (manifest[@"ios"] && manifest[@"ios"][@"backgroundColor"]) { 506 return manifest[@"ios"][@"backgroundColor"]; 507 } 508 return manifest[@"backgroundColor"]; 509} 510 511 512#pragma mark - Internal 513 514- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error 515{ 516 EXAssertMainThread(); 517 _dtmLastFatalErrorShown = [NSDate date]; 518 if (_errorView && _contentView == _errorView) { 519 // already showing, just update 520 _errorView.type = type; 521 _errorView.error = error; 522 } { 523 [_contentView removeFromSuperview]; 524 if (!_errorView) { 525 _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)]; 526 _errorView.delegate = self; 527 _errorView.appRecord = _appRecord; 528 } 529 _errorView.type = type; 530 _errorView.error = error; 531 _contentView = _errorView; 532 [self.view addSubview:_contentView]; 533 [[EXAnalytics sharedInstance] logErrorVisibleEvent]; 534 } 535} 536 537- (void)setIsLoading:(BOOL)isLoading 538{ 539 if ([_appRecord.appManager enablesDeveloperTools] && _dtmLastFatalErrorShown) { 540 if ([_dtmLastFatalErrorShown timeIntervalSinceNow] >= -kEXDevelopmentErrorCoolDownSeconds) { 541 // we just showed a fatal error very recently, do not begin loading. 542 // this can happen in some cases where react native sends the 'started loading' notif 543 // in spite of a packager error. 544 return; 545 } 546 } 547 _isLoading = isLoading; 548 dispatch_async(dispatch_get_main_queue(), ^{ 549 if (isLoading) { 550 self.loadingView.hidden = NO; 551 [self.view bringSubviewToFront:self.loadingView]; 552 } else { 553 self.loadingView.hidden = YES; 554 } 555 }); 556} 557 558#pragma mark - error recovery 559 560- (BOOL)_willAutoRecoverFromError:(NSError *)error 561{ 562 if (![_appRecord.appManager enablesDeveloperTools]) { 563 BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdShouldReloadOnError:_appRecord.experienceId]; 564 if (shouldRecover) { 565 [self _invalidateRecoveryTimer]; 566 _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds 567 target:self 568 selector:@selector(refresh) 569 userInfo:nil 570 repeats:NO]; 571 } 572 return shouldRecover; 573 } 574 return NO; 575} 576 577- (void)_invalidateRecoveryTimer 578{ 579 if (_tmrAutoReloadDebounce) { 580 [_tmrAutoReloadDebounce invalidate]; 581 _tmrAutoReloadDebounce = nil; 582 } 583} 584 585@end 586 587NS_ASSUME_NONNULL_END 588