1// Copyright 2015-present 650 Industries. All rights reserved. 2 3@import UIKit; 4 5#import "EXAppDelegate.h" 6#import "EXAppViewController.h" 7#import "EXHomeAppManager.h" 8#import "EXKernel.h" 9#import "EXAppLoader.h" 10#import "EXKernelAppRecord.h" 11#import "EXKernelAppRegistry.h" 12#import "EXKernelLinkingManager.h" 13#import "EXKernelServiceRegistry.h" 14#import "EXMenuViewController.h" 15#import "EXRootViewController.h" 16 17NSString * const kEXHomeDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey"; 18NSString * const kEXHomeIsNuxFinishedDefaultsKey = @"EXHomeIsNuxFinishedDefaultsKey"; 19 20NS_ASSUME_NONNULL_BEGIN 21 22@interface EXRootViewController () <EXAppBrowserController> 23 24@property (nonatomic, strong) EXMenuViewController *menuViewController; 25@property (nonatomic, assign) BOOL isMenuVisible; 26@property (nonatomic, assign) BOOL isAnimatingAppTransition; 27@property (nonatomic, strong, nullable) NSNumber *orientationBeforeShowingMenu; 28 29@end 30 31@implementation EXRootViewController 32 33- (instancetype)init 34{ 35 if (self = [super init]) { 36 [EXKernel sharedInstance].browserController = self; 37 [self _maybeResetNuxState]; 38 } 39 return self; 40} 41 42/** 43 * Overrides UIViewController's method that returns interface orientations that the view controller supports. 44 * If EXMenuViewController is currently shown we want to use its supported orientations so the UI rotates 45 * when we open the dev menu while in the unsupported orientation. 46 * Otherwise, returns interface orientations supported by the current experience. 47 */ 48- (UIInterfaceOrientationMask)supportedInterfaceOrientations 49{ 50 return _isMenuVisible ? [_menuViewController supportedInterfaceOrientations] : [self.contentViewController supportedInterfaceOrientations]; 51} 52 53/** 54 * Same case as above with `supportedInterfaceOrientations` method. 55 * If we don't override this, we can get incorrect orientation while changing device orientation when the dev menu is visible. 56 */ 57- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation 58{ 59 return _isMenuVisible ? [_menuViewController preferredInterfaceOrientationForPresentation] : [self.contentViewController preferredInterfaceOrientationForPresentation]; 60} 61 62#pragma mark - EXViewController 63 64- (void)createRootAppAndMakeVisible 65{ 66 EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init]; 67 EXAppLoader *homeAppLoader = [[EXAppLoader alloc] initWithLocalManifest:[EXHomeAppManager bundledHomeManifest]]; 68 EXKernelAppRecord *homeAppRecord = [[EXKernelAppRecord alloc] initWithAppLoader:homeAppLoader appManager:homeAppManager]; 69 [[EXKernel sharedInstance].appRegistry registerHomeAppRecord:homeAppRecord]; 70 [self moveAppToVisible:homeAppRecord]; 71} 72 73#pragma mark - EXAppBrowserController 74 75- (void)moveAppToVisible:(EXKernelAppRecord *)appRecord 76{ 77 [self _foregroundAppRecord:appRecord]; 78 79 // When foregrounding the app record we want to add it to the history to handle the edge case 80 // where a user opened a project, then went to home and cleared history, then went back to a 81 // the already open project. 82 [self addHistoryItemWithUrl:appRecord.appLoader.manifestUrl manifest:appRecord.appLoader.manifest]; 83 84} 85 86- (void)toggleMenuWithCompletion:(void (^ _Nullable)(void))completion 87{ 88 [self setIsMenuVisible:!_isMenuVisible completion:completion]; 89} 90 91/** 92 * Sets the visibility of the dev menu and attempts to rotate the UI according to interface orientations supported by the view controller that is on top. 93 */ 94- (void)setIsMenuVisible:(BOOL)isMenuVisible completion:(void (^ _Nullable)(void))completion 95{ 96 if (!_menuViewController) { 97 _menuViewController = [[EXMenuViewController alloc] init]; 98 } 99 if (isMenuVisible != _isMenuVisible) { 100 _isMenuVisible = isMenuVisible; 101 102 if (isMenuVisible) { 103 // We need to force the device to use portrait orientation as the dev menu doesn't support landscape. 104 // However, when removing it, we should set it back to the orientation from before showing the dev menu. 105 _orientationBeforeShowingMenu = [[UIDevice currentDevice] valueForKey:@"orientation"]; 106 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait) forKey:@"orientation"]; 107 } else { 108 // Restore the original orientation that had been set before the dev menu was displayed. 109 [[UIDevice currentDevice] setValue:_orientationBeforeShowingMenu forKey:@"orientation"]; 110 } 111 112 // Ask the system to rotate the UI to device orientation that we've just set to fake value (see previous line of code). 113 [UIViewController attemptRotationToDeviceOrientation]; 114 115 if (isMenuVisible) { 116 // Add menu view controller as a child of the root view controller. 117 [_menuViewController willMoveToParentViewController:self]; 118 [_menuViewController.view setFrame:self.view.frame]; 119 [self.view addSubview:_menuViewController.view]; 120 [_menuViewController didMoveToParentViewController:self]; 121 } else { 122 // Detach menu view controller from the root view controller. 123 [_menuViewController willMoveToParentViewController:nil]; 124 [_menuViewController.view removeFromSuperview]; 125 [_menuViewController didMoveToParentViewController:nil]; 126 } 127 } 128 if (completion) { 129 completion(); 130 } 131} 132 133- (BOOL)isMenuVisible 134{ 135 return _isMenuVisible; 136} 137 138- (void)showQRReader 139{ 140 [self moveHomeToVisible]; 141 [[self _getHomeAppManager] showQRReader]; 142} 143 144- (void)moveHomeToVisible 145{ 146 __weak typeof(self) weakSelf = self; 147 [self setIsMenuVisible:NO completion:^{ 148 __strong typeof(weakSelf) strongSelf = weakSelf; 149 if (strongSelf) { 150 [strongSelf moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord]; 151 152 if (strongSelf.isMenuVisible) { 153 [strongSelf setIsMenuVisible:NO completion:nil]; 154 } 155 } 156 }]; 157} 158 159// this is different from Util.reload() 160// because it can work even on an errored app record (e.g. with no manifest, or with no running bridge). 161- (void)reloadVisibleApp 162{ 163 if (_isMenuVisible) { 164 [self setIsMenuVisible:NO completion:nil]; 165 } 166 167 EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp; 168 [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp]; 169 NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl; 170 171 // Unregister visible app record so all modules get destroyed. 172 [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp]; 173 174 // Create new app record. 175 [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil]; 176} 177 178- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(NSDictionary *)manifest 179{ 180 [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest]; 181} 182 183- (void)getHistoryUrlForExperienceId:(NSString *)experienceId completion:(void (^)(NSString *))completion 184{ 185 return [[self _getHomeAppManager] getHistoryUrlForExperienceId:experienceId completion:completion]; 186} 187 188- (void)setIsNuxFinished:(BOOL)isFinished 189{ 190 [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey]; 191 [[NSUserDefaults standardUserDefaults] synchronize]; 192} 193 194- (BOOL)isNuxFinished 195{ 196 return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey]; 197} 198 199- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord 200{ 201 // show nux if needed 202 if (!self.isNuxFinished 203 && appRecord == [EXKernel sharedInstance].visibleApp 204 && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord 205 && !self.isMenuVisible) { 206 [self setIsMenuVisible:YES completion:nil]; 207 } 208} 209 210#pragma mark - internal 211 212- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord 213{ 214 if (_isAnimatingAppTransition) { 215 return; 216 } 217 EXAppViewController *viewControllerToShow = appRecord.viewController; 218 EXAppViewController *viewControllerToHide; 219 if (viewControllerToShow != self.contentViewController) { 220 _isAnimatingAppTransition = YES; 221 if (self.contentViewController) { 222 viewControllerToHide = (EXAppViewController *)self.contentViewController; 223 } 224 if (viewControllerToShow) { 225 [viewControllerToShow willMoveToParentViewController:self]; 226 [self.view addSubview:viewControllerToShow.view]; 227 [viewControllerToShow foregroundControllers]; 228 } 229 230 __weak typeof(self) weakSelf = self; 231 void (^transitionFinished)(void) = ^{ 232 __strong typeof(weakSelf) strongSelf = weakSelf; 233 if (strongSelf) { 234 if (viewControllerToHide) { 235 // backgrounds and then dismisses all modals that are presented by the app 236 [viewControllerToHide backgroundControllers]; 237 [viewControllerToHide dismissViewControllerAnimated:NO completion:nil]; 238 [viewControllerToHide willMoveToParentViewController:nil]; 239 [viewControllerToHide.view removeFromSuperview]; 240 [viewControllerToHide didMoveToParentViewController:nil]; 241 } 242 if (viewControllerToShow) { 243 [viewControllerToShow didMoveToParentViewController:strongSelf]; 244 strongSelf.contentViewController = viewControllerToShow; 245 } 246 [strongSelf.view setNeedsLayout]; 247 strongSelf.isAnimatingAppTransition = NO; 248 if (strongSelf.delegate) { 249 [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord]; 250 } 251 } 252 }; 253 254 BOOL animated = (viewControllerToHide && viewControllerToShow); 255 if (animated) { 256 if (viewControllerToHide.contentView) { 257 viewControllerToHide.contentView.transform = CGAffineTransformIdentity; 258 viewControllerToHide.contentView.alpha = 1.0f; 259 } 260 if (viewControllerToShow.contentView) { 261 viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f); 262 viewControllerToShow.contentView.alpha = 0; 263 } 264 [UIView animateWithDuration:0.3f animations:^{ 265 if (viewControllerToHide.contentView) { 266 viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f); 267 viewControllerToHide.contentView.alpha = 0.5f; 268 } 269 if (viewControllerToShow.contentView) { 270 viewControllerToShow.contentView.transform = CGAffineTransformIdentity; 271 viewControllerToShow.contentView.alpha = 1.0f; 272 } 273 } completion:^(BOOL finished) { 274 transitionFinished(); 275 }]; 276 } else { 277 transitionFinished(); 278 } 279 } 280} 281 282- (EXHomeAppManager *)_getHomeAppManager 283{ 284 return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager; 285} 286 287- (void)_maybeResetNuxState 288{ 289 // used by appetize: optionally disable nux 290 BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey]; 291 if (disableNuxDefaultsValue) { 292 [self setIsNuxFinished:YES]; 293 [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey]; 294 } 295} 296 297@end 298 299NS_ASSUME_NONNULL_END 300