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 "EXUpdatesManager.h" 18 19#import <React/RCTUtils.h> 20 21#define EX_INTERFACE_ORIENTATION_USE_MANIFEST 0 22 23// when we encounter an error and auto-refresh, we may actually see a series of errors. 24// we only want to trigger refresh once, so we debounce refresh on a timer. 25const CGFloat kEXAutoReloadDebounceSeconds = 0.1; 26 27// in development only, some errors can happen before we even start loading 28// (e.g. certain packager errors, such as an invalid bundle url) 29// and we want to make sure not to cover the error with a loading view or other chrome. 30const CGFloat kEXDevelopmentErrorCoolDownSeconds = 0.1; 31 32NS_ASSUME_NONNULL_BEGIN 33 34@interface EXAppViewController () 35 <EXReactAppManagerUIDelegate, EXAppLoaderDelegate, EXErrorViewDelegate> 36 37@property (nonatomic, assign) BOOL isLoading; 38@property (nonatomic, assign) BOOL isBridgeAlreadyLoading; 39@property (nonatomic, weak) EXKernelAppRecord *appRecord; 40@property (nonatomic, strong) EXAppLoadingView *loadingView; 41@property (nonatomic, strong) EXErrorView *errorView; 42@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; // override super 43@property (nonatomic, strong) NSTimer *tmrAutoReloadDebounce; 44@property (nonatomic, strong) NSDate *dtmLastFatalErrorShown; 45@property (nonatomic, strong) NSMutableArray<UIViewController *> *backgroundedControllers; 46 47@end 48 49@implementation EXAppViewController 50 51@synthesize supportedInterfaceOrientations = _supportedInterfaceOrientations; 52 53#pragma mark - Lifecycle 54 55- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record 56{ 57 if (self = [super init]) { 58 _appRecord = record; 59 _supportedInterfaceOrientations = EX_INTERFACE_ORIENTATION_USE_MANIFEST; 60 } 61 return self; 62} 63 64- (void)dealloc 65{ 66 [self _invalidateRecoveryTimer]; 67 [[NSNotificationCenter defaultCenter] removeObserver:self]; 68} 69 70- (void)viewDidLoad 71{ 72 [super viewDidLoad]; 73 self.view.backgroundColor = [UIColor whiteColor]; 74 75 _loadingView = [[EXAppLoadingView alloc] initWithAppRecord:_appRecord]; 76 [self.view addSubview:_loadingView]; 77 _appRecord.appManager.delegate = self; 78 self.isLoading = YES; 79} 80 81- (void)viewDidAppear:(BOOL)animated 82{ 83 [super viewDidAppear:animated]; 84 if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) { 85 _appRecord.appLoader.delegate = self; 86 _appRecord.appLoader.dataSource = _appRecord.appManager; 87 [self refresh]; 88 } 89} 90 91- (BOOL)shouldAutorotate 92{ 93 return YES; 94} 95 96- (void)viewWillLayoutSubviews 97{ 98 [super viewWillLayoutSubviews]; 99 if (_loadingView) { 100 _loadingView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 101 } 102 if (_contentView) { 103 _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 104 } 105} 106 107#pragma mark - Public 108 109- (void)maybeShowError:(NSError *)error 110{ 111 self.isLoading = NO; 112 if ([self _willAutoRecoverFromError:error]) { 113 return; 114 } 115 if (error && ![error isKindOfClass:[NSError class]]) { 116#if DEBUG 117 NSAssert(NO, @"AppViewController error handler was called on an object that isn't an NSError"); 118#endif 119 return; 120 } 121 122 // we don't ever want to show any Expo UI in a production standalone app, so hard crash 123 if ([EXEnvironment sharedEnvironment].isDetached && ![_appRecord.appManager enablesDeveloperTools]) { 124 NSException *e = [NSException exceptionWithName:@"ExpoFatalError" 125 reason:[NSString stringWithFormat:@"Expo encountered a fatal error: %@", [error localizedDescription]] 126 userInfo:@{NSUnderlyingErrorKey: error}]; 127 @throw e; 128 } 129 130 NSString *domain = (error && error.domain) ? error.domain : @""; 131 BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:EXNetworkErrorDomain]); 132 133 if (isNetworkError) { 134 // show a human-readable reachability error 135 dispatch_async(dispatch_get_main_queue(), ^{ 136 [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; 137 }); 138 } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { 139 // RCTRedBox already handled this 140 } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { 141 // RCTRedBox already handled this 142 } else { 143 dispatch_async(dispatch_get_main_queue(), ^{ 144 [self _showErrorWithType:kEXFatalErrorTypeException error:error]; 145 }); 146 } 147} 148 149- (void)_rebuildBridge 150{ 151 [self _invalidateRecoveryTimer]; 152 [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:_appRecord]; 153 [_appRecord.appManager rebuildBridge]; 154} 155 156- (void)refresh 157{ 158 self.isLoading = YES; 159 self.isBridgeAlreadyLoading = NO; 160 [self _invalidateRecoveryTimer]; 161 [_appRecord.appLoader request]; 162} 163 164- (void)reloadFromCache 165{ 166 self.isLoading = YES; 167 self.isBridgeAlreadyLoading = NO; 168 [self _invalidateRecoveryTimer]; 169 [_appRecord.appLoader requestFromCache]; 170} 171 172- (void)appStateDidBecomeActive 173{ 174 dispatch_async(dispatch_get_main_queue(), ^{ 175 [self _enforceDesiredDeviceOrientation]; 176 }); 177 [_appRecord.appManager appStateDidBecomeActive]; 178} 179 180- (void)appStateDidBecomeInactive 181{ 182 [_appRecord.appManager appStateDidBecomeInactive]; 183} 184 185- (void)_rebuildBridgeWithLoadingViewManifest:(NSDictionary *)manifest 186{ 187 if (!self.isBridgeAlreadyLoading) { 188 self.isBridgeAlreadyLoading = YES; 189 dispatch_async(dispatch_get_main_queue(), ^{ 190 self->_loadingView.manifest = manifest; 191 [self _overrideUserInterfaceStyle]; 192 [self _enforceDesiredDeviceOrientation]; 193 [self _rebuildBridge]; 194 }); 195 } 196} 197 198- (void)foregroundControllers 199{ 200 if (_backgroundedControllers != nil) { 201 __block UIViewController *parentController = self; 202 203 [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) { 204 [parentController presentViewController:viewController animated:NO completion:nil]; 205 parentController = viewController; 206 }]; 207 208 _backgroundedControllers = nil; 209 } 210} 211 212- (void)backgroundControllers 213{ 214 UIViewController *childController = [self presentedViewController]; 215 216 if (childController != nil) { 217 if (_backgroundedControllers == nil) { 218 _backgroundedControllers = [NSMutableArray new]; 219 } 220 221 while (childController != nil) { 222 [_backgroundedControllers addObject:childController]; 223 childController = childController.presentedViewController; 224 } 225 } 226} 227 228#pragma mark - EXAppLoaderDelegate 229 230- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest 231{ 232 if ([EXKernel sharedInstance].browserController) { 233 [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest]; 234 } 235 [self _rebuildBridgeWithLoadingViewManifest:manifest]; 236} 237 238- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress 239{ 240 dispatch_async(dispatch_get_main_queue(), ^{ 241 [self->_loadingView updateStatusWithProgress:progress]; 242 }); 243} 244 245- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data 246{ 247 [self _rebuildBridgeWithLoadingViewManifest:manifest]; 248 if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 249 [self->_appRecord.appManager appLoaderFinished]; 250 } 251} 252 253- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error 254{ 255 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 256 [_appRecord.appManager appLoaderFailedWithError:error]; 257 } 258 [self maybeShowError:error]; 259} 260 261- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(NSDictionary * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error 262{ 263 [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error]; 264} 265 266#pragma mark - EXReactAppManagerDelegate 267 268- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager 269{ 270 UIView *reactView = appManager.rootView; 271 reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 272 reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 273 reactView.backgroundColor = [UIColor clearColor]; 274 275 [_contentView removeFromSuperview]; 276 _contentView = reactView; 277 [self.view addSubview:_contentView]; 278 [self.view sendSubviewToBack:_contentView]; 279 280 [reactView becomeFirstResponder]; 281} 282 283- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager 284{ 285 EXAssertMainThread(); 286 self.isLoading = YES; 287} 288 289- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager 290{ 291 EXAssertMainThread(); 292 self.isLoading = NO; 293 if ([EXKernel sharedInstance].browserController) { 294 [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord]; 295 } 296} 297 298- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error 299{ 300 EXAssertMainThread(); 301 [self maybeShowError:error]; 302} 303 304- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager 305{ 306 307} 308 309- (void)errorViewDidSelectRetry:(EXErrorView *)errorView 310{ 311 [self refresh]; 312} 313 314#pragma mark - orientation 315 316- (UIInterfaceOrientationMask)supportedInterfaceOrientations 317{ 318 if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) { 319 return _supportedInterfaceOrientations; 320 } 321 if (_appRecord.appLoader.manifest) { 322 NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"]; 323 if ([orientationConfig isEqualToString:@"portrait"]) { 324 // lock to portrait 325 return UIInterfaceOrientationMaskPortrait; 326 } else if ([orientationConfig isEqualToString:@"landscape"]) { 327 // lock to landscape 328 return UIInterfaceOrientationMaskLandscape; 329 } 330 } 331 // no config or default value: allow autorotation 332 return UIInterfaceOrientationMaskAllButUpsideDown; 333} 334 335- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations 336{ 337 _supportedInterfaceOrientations = supportedInterfaceOrientations; 338 [self _enforceDesiredDeviceOrientation]; 339} 340 341- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection { 342 [super traitCollectionDidChange:previousTraitCollection]; 343 if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass) 344 || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) { 345 [[EXKernel sharedInstance].serviceRegistry.screenOrientationManager handleScreenOrientationChange:self.traitCollection]; 346 } 347} 348 349- (void)_enforceDesiredDeviceOrientation 350{ 351 RCTAssertMainQueue(); 352 UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations]; 353 UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation]; 354 UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown; 355 switch (mask) { 356 case UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown: 357 if (!UIDeviceOrientationIsPortrait(currentOrientation)) { 358 newOrientation = UIInterfaceOrientationPortrait; 359 } 360 break; 361 case UIInterfaceOrientationMaskPortrait: 362 newOrientation = UIInterfaceOrientationPortrait; 363 break; 364 case UIInterfaceOrientationMaskPortraitUpsideDown: 365 newOrientation = UIInterfaceOrientationPortraitUpsideDown; 366 break; 367 case UIInterfaceOrientationMaskLandscape: 368 if (!UIDeviceOrientationIsLandscape(currentOrientation)) { 369 newOrientation = UIInterfaceOrientationLandscapeLeft; 370 } 371 break; 372 case UIInterfaceOrientationMaskLandscapeLeft: 373 newOrientation = UIInterfaceOrientationLandscapeLeft; 374 break; 375 case UIInterfaceOrientationMaskLandscapeRight: 376 newOrientation = UIInterfaceOrientationLandscapeRight; 377 break; 378 case UIInterfaceOrientationMaskAllButUpsideDown: 379 if (currentOrientation == UIDeviceOrientationFaceDown) { 380 newOrientation = UIInterfaceOrientationPortrait; 381 } 382 break; 383 default: 384 break; 385 } 386 if (newOrientation != UIInterfaceOrientationUnknown) { 387 [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"]; 388 } 389 [UIViewController attemptRotationToDeviceOrientation]; 390} 391 392#pragma mark - user interface style 393 394- (void)_overrideUserInterfaceStyle 395{ 396 if (@available(iOS 13.0, *)) { 397 NSString *userInterfaceStyle = _appRecord.appLoader.manifest[@"ios"][@"userInterfaceStyle"]; 398 self.overrideUserInterfaceStyle = [self _userInterfaceStyleForString:userInterfaceStyle]; 399 } 400} 401 402- (UIUserInterfaceStyle)_userInterfaceStyleForString:(NSString *)userInterfaceStyleString API_AVAILABLE(ios(12.0)) { 403 if ([userInterfaceStyleString isEqualToString:@"dark"]) { 404 return UIUserInterfaceStyleDark; 405 } 406 if ([userInterfaceStyleString isEqualToString:@"automatic"]) { 407 return UIUserInterfaceStyleUnspecified; 408 } 409 return UIUserInterfaceStyleLight; 410} 411 412#pragma mark - Internal 413 414- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error 415{ 416 EXAssertMainThread(); 417 _dtmLastFatalErrorShown = [NSDate date]; 418 if (_errorView && _contentView == _errorView) { 419 // already showing, just update 420 _errorView.type = type; 421 _errorView.error = error; 422 } { 423 [_contentView removeFromSuperview]; 424 if (!_errorView) { 425 _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)]; 426 _errorView.delegate = self; 427 _errorView.appRecord = _appRecord; 428 } 429 _errorView.type = type; 430 _errorView.error = error; 431 _contentView = _errorView; 432 [self.view addSubview:_contentView]; 433 [[EXAnalytics sharedInstance] logErrorVisibleEvent]; 434 } 435} 436 437- (void)setIsLoading:(BOOL)isLoading 438{ 439 if ([_appRecord.appManager enablesDeveloperTools] && _dtmLastFatalErrorShown) { 440 if ([_dtmLastFatalErrorShown timeIntervalSinceNow] >= -kEXDevelopmentErrorCoolDownSeconds) { 441 // we just showed a fatal error very recently, do not begin loading. 442 // this can happen in some cases where react native sends the 'started loading' notif 443 // in spite of a packager error. 444 return; 445 } 446 } 447 _isLoading = isLoading; 448 dispatch_async(dispatch_get_main_queue(), ^{ 449 if (isLoading) { 450 self.loadingView.hidden = NO; 451 [self.view bringSubviewToFront:self.loadingView]; 452 } else { 453 self.loadingView.hidden = YES; 454 } 455 }); 456} 457 458#pragma mark - error recovery 459 460- (BOOL)_willAutoRecoverFromError:(NSError *)error 461{ 462 if (![_appRecord.appManager enablesDeveloperTools]) { 463 BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdShouldReloadOnError:_appRecord.experienceId]; 464 if (shouldRecover) { 465 [self _invalidateRecoveryTimer]; 466 _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds 467 target:self 468 selector:@selector(refresh) 469 userInfo:nil 470 repeats:NO]; 471 } 472 return shouldRecover; 473 } 474 return NO; 475} 476 477- (void)_invalidateRecoveryTimer 478{ 479 if (_tmrAutoReloadDebounce) { 480 [_tmrAutoReloadDebounce invalidate]; 481 _tmrAutoReloadDebounce = nil; 482 } 483} 484 485@end 486 487NS_ASSUME_NONNULL_END 488