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