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 NSString *domain = (error && error.domain) ? error.domain : @""; 122 BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:EXNetworkErrorDomain]); 123 124 if (isNetworkError) { 125 // show a human-readable reachability error 126 dispatch_async(dispatch_get_main_queue(), ^{ 127 [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; 128 }); 129 } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { 130 // RCTRedBox already handled this 131 } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { 132 // RCTRedBox already handled this 133 } else { 134 dispatch_async(dispatch_get_main_queue(), ^{ 135 [self _showErrorWithType:kEXFatalErrorTypeException error:error]; 136 }); 137 } 138} 139 140- (void)_rebuildBridge 141{ 142 [self _invalidateRecoveryTimer]; 143 [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:_appRecord]; 144 [_appRecord.appManager rebuildBridge]; 145} 146 147- (void)refresh 148{ 149 self.isLoading = YES; 150 self.isBridgeAlreadyLoading = NO; 151 [self _invalidateRecoveryTimer]; 152 [_appRecord.appLoader request]; 153} 154 155- (void)reloadFromCache 156{ 157 self.isLoading = YES; 158 self.isBridgeAlreadyLoading = NO; 159 [self _invalidateRecoveryTimer]; 160 [_appRecord.appLoader requestFromCache]; 161} 162 163- (void)appStateDidBecomeActive 164{ 165 dispatch_async(dispatch_get_main_queue(), ^{ 166 [self _enforceDesiredDeviceOrientation]; 167 }); 168 [_appRecord.appManager appStateDidBecomeActive]; 169} 170 171- (void)appStateDidBecomeInactive 172{ 173 [_appRecord.appManager appStateDidBecomeInactive]; 174} 175 176- (void)_rebuildBridgeWithLoadingViewManifest:(NSDictionary *)manifest 177{ 178 if (!self.isBridgeAlreadyLoading) { 179 self.isBridgeAlreadyLoading = YES; 180 dispatch_async(dispatch_get_main_queue(), ^{ 181 self->_loadingView.manifest = manifest; 182 [self _enforceDesiredDeviceOrientation]; 183 [self _rebuildBridge]; 184 }); 185 } 186} 187 188- (void)foregroundControllers 189{ 190 if (_backgroundedControllers != nil) { 191 __block UIViewController *parentController = self; 192 193 [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) { 194 [parentController presentViewController:viewController animated:NO completion:nil]; 195 parentController = viewController; 196 }]; 197 198 _backgroundedControllers = nil; 199 } 200} 201 202- (void)backgroundControllers 203{ 204 UIViewController *childController = [self presentedViewController]; 205 206 if (childController != nil) { 207 if (_backgroundedControllers == nil) { 208 _backgroundedControllers = [NSMutableArray new]; 209 } 210 211 while (childController != nil) { 212 [_backgroundedControllers addObject:childController]; 213 childController = childController.presentedViewController; 214 } 215 } 216} 217 218#pragma mark - EXAppLoaderDelegate 219 220- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest 221{ 222 if ([EXKernel sharedInstance].browserController) { 223 [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest]; 224 } 225 [self _rebuildBridgeWithLoadingViewManifest:manifest]; 226} 227 228- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress 229{ 230 dispatch_async(dispatch_get_main_queue(), ^{ 231 [self->_loadingView updateStatusWithProgress:progress]; 232 }); 233} 234 235- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data 236{ 237 [self _rebuildBridgeWithLoadingViewManifest:manifest]; 238 if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 239 [self->_appRecord.appManager appLoaderFinished]; 240 } 241} 242 243- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error 244{ 245 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 246 [_appRecord.appManager appLoaderFailedWithError:error]; 247 } 248 [self maybeShowError:error]; 249} 250 251- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(NSDictionary * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error 252{ 253 [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error]; 254} 255 256#pragma mark - EXReactAppManagerDelegate 257 258- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager 259{ 260 UIView *reactView = appManager.rootView; 261 reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 262 reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 263 reactView.backgroundColor = [UIColor clearColor]; 264 265 [_contentView removeFromSuperview]; 266 _contentView = reactView; 267 [self.view addSubview:_contentView]; 268 [self.view sendSubviewToBack:_contentView]; 269 270 [reactView becomeFirstResponder]; 271} 272 273- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager 274{ 275 EXAssertMainThread(); 276 self.isLoading = YES; 277} 278 279- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager 280{ 281 EXAssertMainThread(); 282 self.isLoading = NO; 283 if ([EXKernel sharedInstance].browserController) { 284 [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord]; 285 } 286} 287 288- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error 289{ 290 EXAssertMainThread(); 291 [self maybeShowError:error]; 292} 293 294- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager 295{ 296 297} 298 299- (void)errorViewDidSelectRetry:(EXErrorView *)errorView 300{ 301 [self refresh]; 302} 303 304#pragma mark - orientation 305 306- (UIInterfaceOrientationMask)supportedInterfaceOrientations 307{ 308 if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) { 309 return _supportedInterfaceOrientations; 310 } 311 if (_appRecord.appLoader.manifest) { 312 NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"]; 313 if ([orientationConfig isEqualToString:@"portrait"]) { 314 // lock to portrait 315 return UIInterfaceOrientationMaskPortrait; 316 } else if ([orientationConfig isEqualToString:@"landscape"]) { 317 // lock to landscape 318 return UIInterfaceOrientationMaskLandscape; 319 } 320 } 321 // no config or default value: allow autorotation 322 return UIInterfaceOrientationMaskAllButUpsideDown; 323} 324 325- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations 326{ 327 _supportedInterfaceOrientations = supportedInterfaceOrientations; 328 [self _enforceDesiredDeviceOrientation]; 329} 330 331- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection { 332 [super traitCollectionDidChange:previousTraitCollection]; 333 if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass) 334 || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) { 335 [[EXKernel sharedInstance].serviceRegistry.screenOrientationManager handleScreenOrientationChange:self.traitCollection]; 336 } 337} 338 339- (void)_enforceDesiredDeviceOrientation 340{ 341 RCTAssertMainQueue(); 342 UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations]; 343 UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation]; 344 UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown; 345 switch (mask) { 346 case UIInterfaceOrientationMaskPortrait: 347 if (!UIDeviceOrientationIsPortrait(currentOrientation)) { 348 newOrientation = UIInterfaceOrientationPortrait; 349 } 350 break; 351 case UIInterfaceOrientationMaskPortraitUpsideDown: 352 newOrientation = UIInterfaceOrientationPortraitUpsideDown; 353 break; 354 case UIInterfaceOrientationMaskLandscape: 355 if (!UIDeviceOrientationIsLandscape(currentOrientation)) { 356 newOrientation = UIInterfaceOrientationLandscapeLeft; 357 } 358 break; 359 case UIInterfaceOrientationMaskLandscapeLeft: 360 newOrientation = UIInterfaceOrientationLandscapeLeft; 361 break; 362 case UIInterfaceOrientationMaskLandscapeRight: 363 newOrientation = UIInterfaceOrientationLandscapeRight; 364 break; 365 case UIInterfaceOrientationMaskAllButUpsideDown: 366 if (currentOrientation == UIDeviceOrientationFaceDown) { 367 newOrientation = UIInterfaceOrientationPortrait; 368 } 369 break; 370 default: 371 break; 372 } 373 if (newOrientation != UIInterfaceOrientationUnknown) { 374 [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"]; 375 } 376 [UIViewController attemptRotationToDeviceOrientation]; 377} 378 379#pragma mark - Internal 380 381- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error 382{ 383 EXAssertMainThread(); 384 _dtmLastFatalErrorShown = [NSDate date]; 385 if (_errorView && _contentView == _errorView) { 386 // already showing, just update 387 _errorView.type = type; 388 _errorView.error = error; 389 } { 390 [_contentView removeFromSuperview]; 391 if (!_errorView) { 392 _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)]; 393 _errorView.delegate = self; 394 _errorView.appRecord = _appRecord; 395 } 396 _errorView.type = type; 397 _errorView.error = error; 398 _contentView = _errorView; 399 [self.view addSubview:_contentView]; 400 [[EXAnalytics sharedInstance] logErrorVisibleEvent]; 401 } 402} 403 404- (void)setIsLoading:(BOOL)isLoading 405{ 406 if ([_appRecord.appManager enablesDeveloperTools] && _dtmLastFatalErrorShown) { 407 if ([_dtmLastFatalErrorShown timeIntervalSinceNow] >= -kEXDevelopmentErrorCoolDownSeconds) { 408 // we just showed a fatal error very recently, do not begin loading. 409 // this can happen in some cases where react native sends the 'started loading' notif 410 // in spite of a packager error. 411 return; 412 } 413 } 414 _isLoading = isLoading; 415 dispatch_async(dispatch_get_main_queue(), ^{ 416 if (isLoading) { 417 self.loadingView.hidden = NO; 418 [self.view bringSubviewToFront:self.loadingView]; 419 } else { 420 self.loadingView.hidden = YES; 421 } 422 }); 423} 424 425#pragma mark - error recovery 426 427- (BOOL)_willAutoRecoverFromError:(NSError *)error 428{ 429 if (![_appRecord.appManager enablesDeveloperTools]) { 430 BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdShouldReloadOnError:_appRecord.experienceId]; 431 if (shouldRecover) { 432 [self _invalidateRecoveryTimer]; 433 _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds 434 target:self 435 selector:@selector(refresh) 436 userInfo:nil 437 repeats:NO]; 438 } 439 return shouldRecover; 440 } 441 return NO; 442} 443 444- (void)_invalidateRecoveryTimer 445{ 446 if (_tmrAutoReloadDebounce) { 447 [_tmrAutoReloadDebounce invalidate]; 448 _tmrAutoReloadDebounce = nil; 449 } 450} 451 452@end 453 454NS_ASSUME_NONNULL_END 455