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