1// Copyright 2015-present 650 Industries. All rights reserved. 2 3@import UIKit; 4 5#import "EXAppDelegate.h" 6#import "EXAppViewController.h" 7#import "EXButtonView.h" 8#import "EXHomeAppManager.h" 9#import "EXHomeDiagnosticsViewController.h" 10#import "EXKernel.h" 11#import "EXAppLoader.h" 12#import "EXKernelAppRecord.h" 13#import "EXKernelAppRegistry.h" 14#import "EXKernelDevKeyCommands.h" 15#import "EXKernelLinkingManager.h" 16#import "EXKernelServiceRegistry.h" 17#import "EXMenuGestureRecognizer.h" 18#import "EXMenuViewController.h" 19#import "EXRootViewController.h" 20#import "EXMenuWindow.h" 21 22NSString * const kEXHomeDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey"; 23NSString * const kEXHomeIsNuxFinishedDefaultsKey = @"EXHomeIsNuxFinishedDefaultsKey"; 24 25NS_ASSUME_NONNULL_BEGIN 26 27@interface EXRootViewController () <EXAppBrowserController> 28 29@property (nonatomic, strong) EXMenuViewController *menuViewController; 30@property (nonatomic, assign) BOOL isMenuVisible; 31@property (nonatomic, assign) BOOL isAnimatingMenu; 32@property (nonatomic, assign) BOOL isAnimatingAppTransition; 33@property (nonatomic, strong) EXButtonView *btnMenu; 34@property (nonatomic, strong, nullable) EXMenuWindow *menuWindow; 35 36@end 37 38@implementation EXRootViewController 39 40- (instancetype)init 41{ 42 if (self = [super init]) { 43 [EXKernel sharedInstance].browserController = self; 44 [[NSNotificationCenter defaultCenter] addObserver:self 45 selector:@selector(_updateMenuButtonBehavior) 46 name:kEXKernelDidChangeMenuBehaviorNotification 47 object:nil]; 48 [self _maybeResetNuxState]; 49 } 50 return self; 51} 52 53- (void)viewDidLoad 54{ 55 [super viewDidLoad]; 56 _btnMenu = [[EXButtonView alloc] init]; 57 _btnMenu.hidden = YES; 58 [self.view addSubview:_btnMenu]; 59 EXMenuGestureRecognizer *menuGestureRecognizer = [[EXMenuGestureRecognizer alloc] initWithTarget:self action:@selector(_onMenuGestureRecognized:)]; 60 [((EXAppDelegate *)[UIApplication sharedApplication].delegate).window addGestureRecognizer:menuGestureRecognizer]; 61} 62 63- (void)viewWillLayoutSubviews 64{ 65 [super viewWillLayoutSubviews]; 66 _btnMenu.frame = CGRectMake(0, 0, 48.0f, 48.0f); 67 _btnMenu.center = CGPointMake(self.view.frame.size.width - 36.0f, self.view.frame.size.height - 72.0f); 68 [self.view bringSubviewToFront:_btnMenu]; 69} 70 71#pragma mark - EXViewController 72 73- (void)createRootAppAndMakeVisible 74{ 75 EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init]; 76 EXAppLoader *homeAppLoader = [[EXAppLoader alloc] initWithLocalManifest:[EXHomeAppManager bundledHomeManifest]]; 77 EXKernelAppRecord *homeAppRecord = [[EXKernelAppRecord alloc] initWithAppLoader:homeAppLoader appManager:homeAppManager]; 78 [[EXKernel sharedInstance].appRegistry registerHomeAppRecord:homeAppRecord]; 79 [self moveAppToVisible:homeAppRecord]; 80} 81 82#pragma mark - EXAppBrowserController 83 84- (void)moveAppToVisible:(EXKernelAppRecord *)appRecord 85{ 86 [self _foregroundAppRecord:appRecord]; 87 88 // When foregrounding the app record we want to add it to the history to handle the edge case 89 // where a user opened a project, then went to home and cleared history, then went back to a 90 // the already open project. 91 [self addHistoryItemWithUrl:appRecord.appLoader.manifestUrl manifest:appRecord.appLoader.manifest]; 92 93} 94 95- (void)toggleMenuWithCompletion:(void (^ _Nullable)(void))completion 96{ 97 [self setIsMenuVisible:!_isMenuVisible completion:completion]; 98} 99 100- (void)setIsMenuVisible:(BOOL)isMenuVisible completion:(void (^ _Nullable)(void))completion 101{ 102 if (!_menuViewController) { 103 _menuViewController = [[EXMenuViewController alloc] init]; 104 } 105 if (isMenuVisible != _isMenuVisible) { 106 if (!_isAnimatingMenu) { 107 _isMenuVisible = isMenuVisible; 108 [self _animateMenuToVisible:_isMenuVisible completion:completion]; 109 } 110 } else { 111 completion(); 112 } 113} 114 115- (void)showDiagnostics 116{ 117 __weak typeof(self) weakSelf = self; 118 [self setIsMenuVisible:NO completion:^{ 119 __strong typeof(weakSelf) strongSelf = weakSelf; 120 if (strongSelf) { 121 EXHomeDiagnosticsViewController *vcDiagnostics = [[EXHomeDiagnosticsViewController alloc] init]; 122 [strongSelf presentViewController:vcDiagnostics animated:NO completion:nil]; 123 } 124 }]; 125} 126 127- (void)showQRReader 128{ 129 [self moveHomeToVisible]; 130 [[self _getHomeAppManager] showQRReader]; 131} 132 133- (void)moveHomeToVisible 134{ 135 __weak typeof(self) weakSelf = self; 136 [self setIsMenuVisible:NO completion:^{ 137 __strong typeof(weakSelf) strongSelf = weakSelf; 138 if (strongSelf) { 139 [strongSelf moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord]; 140 141 if (strongSelf.isMenuVisible) { 142 [strongSelf setIsMenuVisible:NO completion:nil]; 143 } 144 } 145 }]; 146} 147 148// this is different from Util.reload() 149// because it can work even on an errored app record (e.g. with no manifest, or with no running bridge). 150- (void)reloadVisibleApp 151{ 152 if (_isMenuVisible) { 153 [self setIsMenuVisible:NO completion:nil]; 154 } 155 156 EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp; 157 [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp]; 158 NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl; 159 160 // Unregister visible app record so all modules get destroyed. 161 [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp]; 162 163 // Create new app record. 164 [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil]; 165} 166 167- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(NSDictionary *)manifest 168{ 169 [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest]; 170} 171 172- (void)getHistoryUrlForExperienceId:(NSString *)experienceId completion:(void (^)(NSString *))completion 173{ 174 return [[self _getHomeAppManager] getHistoryUrlForExperienceId:experienceId completion:completion]; 175} 176 177- (void)setIsNuxFinished:(BOOL)isFinished 178{ 179 [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey]; 180 [[NSUserDefaults standardUserDefaults] synchronize]; 181} 182 183- (BOOL)isNuxFinished 184{ 185 return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey]; 186} 187 188- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord 189{ 190 // show nux if needed 191 if (!self.isNuxFinished 192 && appRecord == [EXKernel sharedInstance].visibleApp 193 && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord 194 && !self.isMenuVisible) { 195 [self setIsMenuVisible:YES completion:nil]; 196 } 197 198 // check button availability when any new app loads 199 [self _updateMenuButtonBehavior]; 200} 201 202#pragma mark - internal 203 204- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord 205{ 206 if (_isAnimatingAppTransition) { 207 return; 208 } 209 EXAppViewController *viewControllerToShow = appRecord.viewController; 210 EXAppViewController *viewControllerToHide; 211 if (viewControllerToShow != self.contentViewController) { 212 _isAnimatingAppTransition = YES; 213 if (self.contentViewController) { 214 viewControllerToHide = (EXAppViewController *)self.contentViewController; 215 } 216 if (viewControllerToShow) { 217 [viewControllerToShow willMoveToParentViewController:self]; 218 [self.view addSubview:viewControllerToShow.view]; 219 [viewControllerToShow foregroundControllers]; 220 } 221 222 __weak typeof(self) weakSelf = self; 223 void (^transitionFinished)(void) = ^{ 224 __strong typeof(weakSelf) strongSelf = weakSelf; 225 if (strongSelf) { 226 if (viewControllerToHide) { 227 // backgrounds and then dismisses all modals that are presented by the app 228 [viewControllerToHide backgroundControllers]; 229 [viewControllerToHide dismissViewControllerAnimated:NO completion:nil]; 230 [viewControllerToHide willMoveToParentViewController:nil]; 231 [viewControllerToHide.view removeFromSuperview]; 232 [viewControllerToHide didMoveToParentViewController:nil]; 233 } 234 if (viewControllerToShow) { 235 [viewControllerToShow didMoveToParentViewController:strongSelf]; 236 strongSelf.contentViewController = viewControllerToShow; 237 } 238 [strongSelf.view setNeedsLayout]; 239 strongSelf.isAnimatingAppTransition = NO; 240 if (strongSelf.delegate) { 241 [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord]; 242 } 243 } 244 }; 245 246 BOOL animated = (viewControllerToHide && viewControllerToShow); 247 if (animated) { 248 if (viewControllerToHide.contentView) { 249 viewControllerToHide.contentView.transform = CGAffineTransformIdentity; 250 viewControllerToHide.contentView.alpha = 1.0f; 251 } 252 if (viewControllerToShow.contentView) { 253 viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f); 254 viewControllerToShow.contentView.alpha = 0; 255 } 256 [UIView animateWithDuration:0.3f animations:^{ 257 if (viewControllerToHide.contentView) { 258 viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f); 259 viewControllerToHide.contentView.alpha = 0.5f; 260 } 261 if (viewControllerToShow.contentView) { 262 viewControllerToShow.contentView.transform = CGAffineTransformIdentity; 263 viewControllerToShow.contentView.alpha = 1.0f; 264 } 265 } completion:^(BOOL finished) { 266 transitionFinished(); 267 }]; 268 } else { 269 transitionFinished(); 270 } 271 } 272} 273 274- (void)_animateMenuToVisible:(BOOL)visible completion:(void (^ _Nullable)(void))completion 275{ 276 _isAnimatingMenu = YES; 277 __weak typeof(self) weakSelf = self; 278 if (visible) { 279 [_menuViewController willMoveToParentViewController:self]; 280 281 if (_menuWindow == nil) { 282 _menuWindow = [[EXMenuWindow alloc] init]; 283 } 284 285 [_menuWindow setFrame:self.view.frame]; 286 [_menuWindow addSubview:_menuViewController.view]; 287 [_menuWindow makeKeyAndVisible]; 288 289 _menuViewController.view.alpha = 0.0f; 290 _menuViewController.view.transform = CGAffineTransformMakeScale(1.1f, 1.1f); 291 [UIView animateWithDuration:0.1f animations:^{ 292 self.menuViewController.view.alpha = 1.0f; 293 self.menuViewController.view.transform = CGAffineTransformIdentity; 294 } completion:^(BOOL finished) { 295 __strong typeof(weakSelf) strongSelf = weakSelf; 296 if (strongSelf) { 297 strongSelf.isAnimatingMenu = NO; 298 [strongSelf.menuViewController didMoveToParentViewController:self]; 299 if (completion) { 300 completion(); 301 } 302 } 303 }]; 304 } else { 305 _menuViewController.view.alpha = 1.0f; 306 [UIView animateWithDuration:0.1f animations:^{ 307 self.menuViewController.view.alpha = 0.0f; 308 } completion:^(BOOL finished) { 309 __strong typeof(weakSelf) strongSelf = weakSelf; 310 if (strongSelf) { 311 strongSelf.isAnimatingMenu = NO; 312 [strongSelf.menuViewController willMoveToParentViewController:nil]; 313 [strongSelf.menuViewController.view removeFromSuperview]; 314 [strongSelf.menuViewController didMoveToParentViewController:nil]; 315 strongSelf.menuWindow = nil; 316 if (completion) { 317 completion(); 318 } 319 } 320 }]; 321 } 322} 323 324- (EXHomeAppManager *)_getHomeAppManager 325{ 326 return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager; 327} 328 329- (void)_maybeResetNuxState 330{ 331 // used by appetize: optionally disable nux 332 BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey]; 333 if (disableNuxDefaultsValue) { 334 [self setIsNuxFinished:YES]; 335 [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey]; 336 } 337} 338 339- (void)_updateMenuButtonBehavior 340{ 341 BOOL shouldShowButton = [[EXKernelDevKeyCommands sharedInstance] isLegacyMenuButtonAvailable]; 342 dispatch_async(dispatch_get_main_queue(), ^{ 343 self.btnMenu.hidden = !shouldShowButton; 344 }); 345} 346 347- (void)_onMenuGestureRecognized:(EXMenuGestureRecognizer *)sender 348{ 349 if (sender.state == UIGestureRecognizerStateEnded) { 350 [[EXKernel sharedInstance] switchTasks]; 351 } 352} 353 354@end 355 356NS_ASSUME_NONNULL_END 357