1// Copyright 2015-present 650 Industries. All rights reserved. 2 3@import UIKit; 4 5#import <ExpoModulesCore/EXDefines.h> 6 7#import "EXAppViewController.h" 8#import "EXHomeAppManager.h" 9#import "EXKernel.h" 10#import "EXDevelopmentHomeLoader.h" 11#import "EXKernelAppRecord.h" 12#import "EXKernelAppRegistry.h" 13#import "EXKernelLinkingManager.h" 14#import "EXKernelServiceRegistry.h" 15#import "EXRootViewController.h" 16#import "EXDevMenuManager.h" 17#import "EXEmbeddedHomeLoader.h" 18#import "EXBuildConstants.h" 19 20@import ExpoScreenOrientation; 21 22NSString * const kEXHomeDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey"; 23NSString * const kEXHomeIsNuxFinishedDefaultsKey = @"EXHomeIsNuxFinishedDefaultsKey"; 24 25NS_ASSUME_NONNULL_BEGIN 26 27@interface EXRootViewController () <EXAppBrowserController> 28 29@property (nonatomic, assign) BOOL isAnimatingAppTransition; 30@property (nonatomic, weak) UIViewController *transitioningToViewController; 31 32@end 33 34@implementation EXRootViewController 35 36- (instancetype)init 37{ 38 if (self = [super init]) { 39 [EXKernel sharedInstance].browserController = self; 40 [self _maybeResetNuxState]; 41 } 42 return self; 43} 44 45#pragma mark - Screen Orientation 46 47- (BOOL)shouldAutorotate 48{ 49 return YES; 50} 51 52/** 53 * supportedInterfaceOrienation has to defined by the currently visible app (to support multiple apps with different settings), 54 * but according to the iOS docs 'Typically, the system calls this method only on the root view controller of the window', 55 * so we need to query the kernel about currently visible app and it's view controller settings 56 */ 57- (UIInterfaceOrientationMask)supportedInterfaceOrientations 58{ 59 // During app transition we want to return the orientation of the screen that will be shown. This makes sure 60 // that the rotation animation starts as the new view controller is being shown. 61 if (_isAnimatingAppTransition && _transitioningToViewController != nil) { 62 return [_transitioningToViewController supportedInterfaceOrientations]; 63 } 64 65 const UIInterfaceOrientationMask visibleAppSupportedInterfaceOrientations = 66 [EXKernel sharedInstance] 67 .visibleApp 68 .viewController 69 .supportedInterfaceOrientations; 70 71 return visibleAppSupportedInterfaceOrientations; 72} 73 74#pragma mark - EXViewController 75 76- (void)createRootAppAndMakeVisible 77{ 78 EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init]; 79 80 // if developing, use development manifest from EXBuildConstants 81 EXAbstractLoader *homeAppLoader; 82 if ([EXBuildConstants sharedInstance].isDevKernel) { 83 homeAppLoader = [[EXDevelopmentHomeLoader alloc] init]; 84 } else { 85 homeAppLoader = [[EXEmbeddedHomeLoader alloc] init]; 86 } 87 88 EXKernelAppRecord *homeAppRecord = [[EXKernelAppRecord alloc] initWithAppLoader:homeAppLoader appManager:homeAppManager]; 89 [[EXKernel sharedInstance].appRegistry registerHomeAppRecord:homeAppRecord]; 90 [self moveAppToVisible:homeAppRecord]; 91} 92 93#pragma mark - EXAppBrowserController 94 95- (void)moveAppToVisible:(EXKernelAppRecord *)appRecord 96{ 97 [self _foregroundAppRecord:appRecord]; 98 99 // When foregrounding the app record we want to add it to the history to handle the edge case 100 // where a user opened a project, then went to home and cleared history, then went back to a 101 // the already open project. 102 [self addHistoryItemWithUrl:appRecord.appLoader.manifestUrl manifest:appRecord.appLoader.manifest]; 103 104} 105 106- (void)showQRReader 107{ 108 [self moveHomeToVisible]; 109 [[self _getHomeAppManager] showQRReader]; 110} 111 112- (void)moveHomeToVisible 113{ 114 [[EXDevMenuManager sharedInstance] close]; 115 [self moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord]; 116} 117 118- (BOOL)_isHomeVisible { 119 return [EXKernel sharedInstance].appRegistry.homeAppRecord == [EXKernel sharedInstance].visibleApp; 120} 121 122// this is different from Util.reload() 123// because it can work even on an errored app record (e.g. with no manifest, or with no running bridge). 124- (void)reloadVisibleApp 125{ 126 if ([self _isHomeVisible]) { 127 EXReactAppManager *homeAppManager = [EXKernel sharedInstance].appRegistry.homeAppRecord.appManager; 128 // reloadBridge will only reload the app if developer tools are enabled for the app 129 [homeAppManager reloadBridge]; 130 return; 131 } 132 133 [[EXDevMenuManager sharedInstance] close]; 134 135 EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp; 136 NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl; 137 138 // Unregister visible app record so all modules get destroyed. 139 [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp]; 140 141 // Create new app record. 142 [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil]; 143} 144 145- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(EXManifestsManifest *)manifest 146{ 147 [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest]; 148} 149 150- (void)getHistoryUrlForScopeKey:(NSString *)scopeKey completion:(void (^)(NSString *))completion 151{ 152 return [[self _getHomeAppManager] getHistoryUrlForScopeKey:scopeKey completion:completion]; 153} 154 155- (void)setIsNuxFinished:(BOOL)isFinished 156{ 157 [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey]; 158 [[NSUserDefaults standardUserDefaults] synchronize]; 159} 160 161- (BOOL)isNuxFinished 162{ 163 return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey]; 164} 165 166- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord 167{ 168 // show nux if needed 169 if (!self.isNuxFinished 170 && appRecord == [EXKernel sharedInstance].visibleApp 171 && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord) { 172 [[EXDevMenuManager sharedInstance] open]; 173 } 174 175 // Re-apply the default orientation after the app has been loaded (eq. after a reload) 176 [self _applySupportedInterfaceOrientations]; 177} 178 179#pragma mark - internal 180 181- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord 182{ 183 // Some transition is in progress 184 if (_isAnimatingAppTransition) { 185 return; 186 } 187 188 EXAppViewController *viewControllerToShow = appRecord.viewController; 189 _transitioningToViewController = viewControllerToShow; 190 191 // Tried to foreground the very same view controller 192 if (viewControllerToShow == self.contentViewController) { 193 return; 194 } 195 196 _isAnimatingAppTransition = YES; 197 198 EXAppViewController *viewControllerToHide = (EXAppViewController *)self.contentViewController; 199 200 if (viewControllerToShow) { 201 [self.view addSubview:viewControllerToShow.view]; 202 [self addChildViewController:viewControllerToShow]; 203 } 204 205 // Try transitioning to the interface orientation of the app before it is shown for smoother transitions 206 [self _applySupportedInterfaceOrientations]; 207 208 EX_WEAKIFY(self) 209 void (^finalizeTransition)(void) = ^{ 210 EX_ENSURE_STRONGIFY(self) 211 if (viewControllerToHide) { 212 // backgrounds and then dismisses all modals that are presented by the app 213 [viewControllerToHide backgroundControllers]; 214 [viewControllerToHide dismissViewControllerAnimated:NO completion:nil]; 215 [viewControllerToHide willMoveToParentViewController:nil]; 216 [viewControllerToHide removeFromParentViewController]; 217 [viewControllerToHide.view removeFromSuperview]; 218 } 219 220 if (viewControllerToShow) { 221 [viewControllerToShow didMoveToParentViewController:self]; 222 self.contentViewController = viewControllerToShow; 223 } 224 225 [self.view setNeedsLayout]; 226 self.isAnimatingAppTransition = NO; 227 self.transitioningToViewController = nil; 228 if (self.delegate) { 229 [self.delegate viewController:self didNavigateAppToVisible:appRecord]; 230 } 231 [self _applySupportedInterfaceOrientations]; 232 }; 233 234 BOOL animated = (viewControllerToHide && viewControllerToShow); 235 if (!animated) { 236 return finalizeTransition(); 237 } 238 239 if (viewControllerToHide.contentView) { 240 viewControllerToHide.contentView.transform = CGAffineTransformIdentity; 241 viewControllerToHide.contentView.alpha = 1.0f; 242 } 243 if (viewControllerToShow.contentView) { 244 viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f); 245 viewControllerToShow.contentView.alpha = 0; 246 } 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 finalizeTransition(); 259 }]; 260} 261 262- (EXHomeAppManager *)_getHomeAppManager 263{ 264 return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager; 265} 266 267- (void)_maybeResetNuxState 268{ 269 // used by appetize: optionally disable nux 270 BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey]; 271 if (disableNuxDefaultsValue) { 272 [self setIsNuxFinished:YES]; 273 [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey]; 274 } 275} 276 277- (void)_applySupportedInterfaceOrientations 278{ 279 if (@available(iOS 16, *)) { 280 [self setNeedsUpdateOfSupportedInterfaceOrientations]; 281 } else { 282 // On iOS < 16 we need to try to rotate to the desired orientation, which also 283 // makes the view controller to update the supported orientations 284 UIInterfaceOrientationMask orientationMask = [self supportedInterfaceOrientations]; 285 [ScreenOrientationRegistry.shared enforceDesiredDeviceOrientationWithOrientationMask:orientationMask]; 286 } 287} 288 289@end 290 291NS_ASSUME_NONNULL_END 292