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