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 89- (void)toggleMenuWithCompletion:(void (^ _Nullable)(void))completion 90{ 91 [self setIsMenuVisible:!_isMenuVisible completion:completion]; 92} 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 if (!_isAnimatingMenu) { 101 _isMenuVisible = isMenuVisible; 102 [self _animateMenuToVisible:_isMenuVisible completion:completion]; 103 } 104 } else { 105 completion(); 106 } 107} 108 109- (void)showDiagnostics 110{ 111 __weak typeof(self) weakSelf = self; 112 [self setIsMenuVisible:NO completion:^{ 113 __strong typeof(weakSelf) strongSelf = weakSelf; 114 if (strongSelf) { 115 EXHomeDiagnosticsViewController *vcDiagnostics = [[EXHomeDiagnosticsViewController alloc] init]; 116 [strongSelf presentViewController:vcDiagnostics animated:NO completion:nil]; 117 } 118 }]; 119} 120 121- (void)showQRReader 122{ 123 [self moveHomeToVisible]; 124 [[self _getHomeAppManager] showQRReader]; 125} 126 127- (void)moveHomeToVisible 128{ 129 __weak typeof(self) weakSelf = self; 130 [self setIsMenuVisible:NO completion:^{ 131 __strong typeof(weakSelf) strongSelf = weakSelf; 132 if (strongSelf) { 133 [strongSelf moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord]; 134 135 if (strongSelf.isMenuVisible) { 136 [strongSelf setIsMenuVisible:NO completion:nil]; 137 } 138 } 139 }]; 140} 141 142- (void)refreshVisibleApp 143{ 144 // this is different from Util.reload() 145 // because it can work even on an errored app record (e.g. with no manifest, or with no running bridge). 146 [self setIsMenuVisible:NO completion:nil]; 147 EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp; 148 [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp]; 149 NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl; 150 [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil]; 151} 152 153- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(NSDictionary *)manifest 154{ 155 [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest]; 156} 157 158- (void)getIsValidHomeManifestToOpen:(NSDictionary *)manifest manifestUrl:(NSURL *) manifestUrl completion:(void (^)(BOOL isValid))completion 159{ 160 [[self _getHomeAppManager] getIsValidHomeManifestToOpen:manifest manifestUrl:(NSURL *) manifestUrl completion:completion]; 161} 162 163- (void)getHistoryUrlForExperienceId:(NSString *)experienceId completion:(void (^)(NSString *))completion 164{ 165 return [[self _getHomeAppManager] getHistoryUrlForExperienceId:experienceId completion:completion]; 166} 167 168- (void)setIsNuxFinished:(BOOL)isFinished 169{ 170 [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey]; 171 [[NSUserDefaults standardUserDefaults] synchronize]; 172} 173 174- (BOOL)isNuxFinished 175{ 176 return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey]; 177} 178 179- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord 180{ 181 // show nux if needed 182 if (!self.isNuxFinished 183 && appRecord == [EXKernel sharedInstance].visibleApp 184 && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord 185 && !self.isMenuVisible) { 186 [self setIsMenuVisible:YES completion:nil]; 187 } 188 189 // check button availability when any new app loads 190 [self _updateMenuButtonBehavior]; 191} 192 193#pragma mark - internal 194 195- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord 196{ 197 if (_isAnimatingAppTransition) { 198 return; 199 } 200 EXAppViewController *viewControllerToShow = appRecord.viewController; 201 EXAppViewController *viewControllerToHide; 202 if (viewControllerToShow != self.contentViewController) { 203 _isAnimatingAppTransition = YES; 204 if (self.contentViewController) { 205 viewControllerToHide = (EXAppViewController *)self.contentViewController; 206 } 207 if (viewControllerToShow) { 208 [viewControllerToShow willMoveToParentViewController:self]; 209 [self.view addSubview:viewControllerToShow.view]; 210 [viewControllerToShow foregroundControllers]; 211 } 212 213 __weak typeof(self) weakSelf = self; 214 void (^transitionFinished)(void) = ^{ 215 __strong typeof(weakSelf) strongSelf = weakSelf; 216 if (strongSelf) { 217 if (viewControllerToHide) { 218 // backgrounds and then dismisses all modals that are presented by the app 219 [viewControllerToHide backgroundControllers]; 220 [viewControllerToHide dismissViewControllerAnimated:NO completion:^{ 221 [viewControllerToHide willMoveToParentViewController:nil]; 222 [viewControllerToHide.view removeFromSuperview]; 223 [viewControllerToHide didMoveToParentViewController:nil]; 224 }]; 225 } 226 if (viewControllerToShow) { 227 [viewControllerToShow didMoveToParentViewController:strongSelf]; 228 strongSelf.contentViewController = viewControllerToShow; 229 } 230 [strongSelf.view setNeedsLayout]; 231 strongSelf.isAnimatingAppTransition = NO; 232 if (strongSelf.delegate) { 233 [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord]; 234 } 235 } 236 }; 237 238 BOOL animated = (viewControllerToHide && viewControllerToShow); 239 if (animated) { 240 if (viewControllerToHide.contentView) { 241 viewControllerToHide.contentView.transform = CGAffineTransformIdentity; 242 viewControllerToHide.contentView.alpha = 1.0f; 243 } 244 if (viewControllerToShow.contentView) { 245 viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f); 246 viewControllerToShow.contentView.alpha = 0; 247 } 248 [UIView animateWithDuration:0.3f animations:^{ 249 if (viewControllerToHide.contentView) { 250 viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f); 251 viewControllerToHide.contentView.alpha = 0.5f; 252 } 253 if (viewControllerToShow.contentView) { 254 viewControllerToShow.contentView.transform = CGAffineTransformIdentity; 255 viewControllerToShow.contentView.alpha = 1.0f; 256 } 257 } completion:^(BOOL finished) { 258 transitionFinished(); 259 }]; 260 } else { 261 transitionFinished(); 262 } 263 } 264} 265 266- (void)_animateMenuToVisible:(BOOL)visible completion:(void (^ _Nullable)(void))completion 267{ 268 _isAnimatingMenu = YES; 269 __weak typeof(self) weakSelf = self; 270 if (visible) { 271 [_menuViewController willMoveToParentViewController:self]; 272 273 if (_menuWindow == nil) { 274 _menuWindow = [[EXMenuWindow alloc] init]; 275 } 276 277 [_menuWindow setFrame:self.view.frame]; 278 [_menuWindow addSubview:_menuViewController.view]; 279 [_menuWindow makeKeyAndVisible]; 280 281 _menuViewController.view.alpha = 0.0f; 282 _menuViewController.view.transform = CGAffineTransformMakeScale(1.1f, 1.1f); 283 [UIView animateWithDuration:0.1f animations:^{ 284 self.menuViewController.view.alpha = 1.0f; 285 self.menuViewController.view.transform = CGAffineTransformIdentity; 286 } completion:^(BOOL finished) { 287 __strong typeof(weakSelf) strongSelf = weakSelf; 288 if (strongSelf) { 289 strongSelf.isAnimatingMenu = NO; 290 [strongSelf.menuViewController didMoveToParentViewController:self]; 291 if (completion) { 292 completion(); 293 } 294 } 295 }]; 296 } else { 297 _menuViewController.view.alpha = 1.0f; 298 [UIView animateWithDuration:0.1f animations:^{ 299 self.menuViewController.view.alpha = 0.0f; 300 } completion:^(BOOL finished) { 301 __strong typeof(weakSelf) strongSelf = weakSelf; 302 if (strongSelf) { 303 strongSelf.isAnimatingMenu = NO; 304 [strongSelf.menuViewController willMoveToParentViewController:nil]; 305 [strongSelf.menuViewController.view removeFromSuperview]; 306 [strongSelf.menuViewController didMoveToParentViewController:nil]; 307 strongSelf.menuWindow = nil; 308 if (completion) { 309 completion(); 310 } 311 } 312 }]; 313 } 314} 315 316- (EXHomeAppManager *)_getHomeAppManager 317{ 318 return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager; 319} 320 321- (void)_maybeResetNuxState 322{ 323 // used by appetize: optionally disable nux 324 BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey]; 325 if (disableNuxDefaultsValue) { 326 [self setIsNuxFinished:YES]; 327 [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey]; 328 } 329} 330 331- (void)_updateMenuButtonBehavior 332{ 333 BOOL shouldShowButton = [[EXKernelDevKeyCommands sharedInstance] isLegacyMenuButtonAvailable]; 334 dispatch_async(dispatch_get_main_queue(), ^{ 335 self.btnMenu.hidden = !shouldShowButton; 336 }); 337} 338 339- (void)_onMenuGestureRecognized:(EXMenuGestureRecognizer *)sender 340{ 341 if (sender.state == UIGestureRecognizerStateEnded) { 342 [[EXKernel sharedInstance] switchTasks]; 343 } 344} 345 346@end 347 348NS_ASSUME_NONNULL_END 349