xref: /expo/ios/Client/EXRootViewController.m (revision 19a0af8d)
17f6367d0SBen Roth// Copyright 2015-present 650 Industries. All rights reserved.
27f6367d0SBen Roth
37f6367d0SBen Roth@import UIKit;
47f6367d0SBen Roth
5c8452ff7SBartłomiej Bukowski#import <ExpoModulesCore/EXDefines.h>
6c8452ff7SBartłomiej Bukowski
77f6367d0SBen Roth#import "EXAppViewController.h"
87f6367d0SBen Roth#import "EXHomeAppManager.h"
97f6367d0SBen Roth#import "EXKernel.h"
10*19a0af8dSWill Schurman#import "EXDevelopmentHomeLoader.h"
117f6367d0SBen Roth#import "EXKernelAppRecord.h"
127f6367d0SBen Roth#import "EXKernelAppRegistry.h"
13f322f621SBen Roth#import "EXKernelLinkingManager.h"
147f6367d0SBen Roth#import "EXKernelServiceRegistry.h"
157f6367d0SBen Roth#import "EXRootViewController.h"
16f67462bcSTomasz Sapeta#import "EXDevMenuManager.h"
17*19a0af8dSWill Schurman#import "EXEmbeddedHomeLoader.h"
18*19a0af8dSWill Schurman#import "EXBuildConstants.h"
197f6367d0SBen Roth
20a859e3aeSWojciech Dróżdż@import ExpoScreenOrientation;
21a859e3aeSWojciech Dróżdż
2266169681SBen RothNSString * const kEXHomeDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey";
2366169681SBen RothNSString * const kEXHomeIsNuxFinishedDefaultsKey = @"EXHomeIsNuxFinishedDefaultsKey";
2466169681SBen Roth
257f6367d0SBen RothNS_ASSUME_NONNULL_BEGIN
267f6367d0SBen Roth
27b987b50fSBen Roth@interface EXRootViewController () <EXAppBrowserController>
287f6367d0SBen Roth
29edc8619eSBen Roth@property (nonatomic, assign) BOOL isAnimatingAppTransition;
30a859e3aeSWojciech Dróżdż@property (nonatomic, weak) UIViewController *transitioningToViewController;
317f6367d0SBen Roth
327f6367d0SBen Roth@end
337f6367d0SBen Roth
347f6367d0SBen Roth@implementation EXRootViewController
357f6367d0SBen Roth
367f6367d0SBen Roth- (instancetype)init
377f6367d0SBen Roth{
387f6367d0SBen Roth  if (self = [super init]) {
397f6367d0SBen Roth    [EXKernel sharedInstance].browserController = self;
4089fd6b24SBen Roth    [self _maybeResetNuxState];
417f6367d0SBen Roth  }
427f6367d0SBen Roth  return self;
437f6367d0SBen Roth}
447f6367d0SBen Roth
4575d03742SBartłomiej Bukowski#pragma mark - Screen Orientation
4675d03742SBartłomiej Bukowski
4775d03742SBartłomiej Bukowski- (BOOL)shouldAutorotate
4875d03742SBartłomiej Bukowski{
4975d03742SBartłomiej Bukowski  return YES;
5075d03742SBartłomiej Bukowski}
5175d03742SBartłomiej Bukowski
5275d03742SBartłomiej Bukowski/**
5375d03742SBartłomiej Bukowski * supportedInterfaceOrienation has to defined by the currently visible app (to support multiple apps with different settings),
5475d03742SBartłomiej Bukowski * but according to the iOS docs 'Typically, the system calls this method only on the root view controller of the window',
5575d03742SBartłomiej Bukowski * so we need to query the kernel about currently visible app and it's view controller settings
5675d03742SBartłomiej Bukowski */
5775d03742SBartłomiej Bukowski- (UIInterfaceOrientationMask)supportedInterfaceOrientations
5875d03742SBartłomiej Bukowski{
59a859e3aeSWojciech Dróżdż  // During app transition we want to return the orientation of the screen that will be shown. This makes sure
60a859e3aeSWojciech Dróżdż  // that the rotation animation starts as the new view controller is being shown.
61a859e3aeSWojciech Dróżdż  if (_isAnimatingAppTransition && _transitioningToViewController != nil) {
62a859e3aeSWojciech Dróżdż    return [_transitioningToViewController supportedInterfaceOrientations];
63a859e3aeSWojciech Dróżdż  }
64a859e3aeSWojciech Dróżdż
6575d03742SBartłomiej Bukowski  const UIInterfaceOrientationMask visibleAppSupportedInterfaceOrientations =
6675d03742SBartłomiej Bukowski    [EXKernel sharedInstance]
6775d03742SBartłomiej Bukowski      .visibleApp
6875d03742SBartłomiej Bukowski      .viewController
6975d03742SBartłomiej Bukowski      .supportedInterfaceOrientations;
70a859e3aeSWojciech Dróżdż
7175d03742SBartłomiej Bukowski  return visibleAppSupportedInterfaceOrientations;
7275d03742SBartłomiej Bukowski}
7375d03742SBartłomiej Bukowski
7489fd6b24SBen Roth#pragma mark - EXViewController
757f6367d0SBen Roth
7689fd6b24SBen Roth- (void)createRootAppAndMakeVisible
7789fd6b24SBen Roth{
787f6367d0SBen Roth  EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init];
79*19a0af8dSWill Schurman
80*19a0af8dSWill Schurman  // if developing, use development manifest from EXBuildConstants
81*19a0af8dSWill Schurman  EXAbstractLoader *homeAppLoader;
82*19a0af8dSWill Schurman  if ([EXBuildConstants sharedInstance].isDevKernel) {
83*19a0af8dSWill Schurman    homeAppLoader = [[EXDevelopmentHomeLoader alloc] init];
84*19a0af8dSWill Schurman  } else {
85*19a0af8dSWill Schurman    homeAppLoader = [[EXEmbeddedHomeLoader alloc] init];
86*19a0af8dSWill Schurman  }
87*19a0af8dSWill Schurman
887f6367d0SBen Roth  EXKernelAppRecord *homeAppRecord = [[EXKernelAppRecord alloc] initWithAppLoader:homeAppLoader appManager:homeAppManager];
897f6367d0SBen Roth  [[EXKernel sharedInstance].appRegistry registerHomeAppRecord:homeAppRecord];
907f6367d0SBen Roth  [self moveAppToVisible:homeAppRecord];
917f6367d0SBen Roth}
927f6367d0SBen Roth
937f6367d0SBen Roth#pragma mark - EXAppBrowserController
947f6367d0SBen Roth
957f6367d0SBen Roth- (void)moveAppToVisible:(EXKernelAppRecord *)appRecord
967f6367d0SBen Roth{
977f6367d0SBen Roth  [self _foregroundAppRecord:appRecord];
98bbef49b2SBrent Vatne
99bbef49b2SBrent Vatne  // When foregrounding the app record we want to add it to the history to handle the edge case
100bbef49b2SBrent Vatne  // where a user opened a project, then went to home and cleared history, then went back to a
101bbef49b2SBrent Vatne  // the already open project.
102bbef49b2SBrent Vatne  [self addHistoryItemWithUrl:appRecord.appLoader.manifestUrl manifest:appRecord.appLoader.manifest];
103373d5b21SBrent Vatne
1047f6367d0SBen Roth}
1057f6367d0SBen Roth
1062fcba380SBen Roth- (void)showQRReader
1072fcba380SBen Roth{
1082fcba380SBen Roth  [self moveHomeToVisible];
109f44e163eSBen Roth  [[self _getHomeAppManager] showQRReader];
110b987b50fSBen Roth}
111b987b50fSBen Roth
112b987b50fSBen Roth- (void)moveHomeToVisible
113b987b50fSBen Roth{
114f67462bcSTomasz Sapeta  [[EXDevMenuManager sharedInstance] close];
115f67462bcSTomasz Sapeta  [self moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord];
116b987b50fSBen Roth}
117b987b50fSBen Roth
118937e5c73SBrent Vatne- (BOOL)_isHomeVisible {
119937e5c73SBrent Vatne  return [EXKernel sharedInstance].appRegistry.homeAppRecord == [EXKernel sharedInstance].visibleApp;
120937e5c73SBrent Vatne}
121937e5c73SBrent Vatne
122928bc3baSBen Roth// this is different from Util.reload()
123928bc3baSBen Roth// because it can work even on an errored app record (e.g. with no manifest, or with no running bridge).
124b333b418SBrent Vatne- (void)reloadVisibleApp
125b333b418SBrent Vatne{
126937e5c73SBrent Vatne  if ([self _isHomeVisible]) {
127937e5c73SBrent Vatne    EXReactAppManager *homeAppManager = [EXKernel sharedInstance].appRegistry.homeAppRecord.appManager;
128937e5c73SBrent Vatne    // reloadBridge will only reload the app if developer tools are enabled for the app
129937e5c73SBrent Vatne    [homeAppManager reloadBridge];
130937e5c73SBrent Vatne    return;
131937e5c73SBrent Vatne  }
132937e5c73SBrent Vatne
133f67462bcSTomasz Sapeta  [[EXDevMenuManager sharedInstance] close];
134b333b418SBrent Vatne
13585f025b0SBen Roth  EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp;
13685f025b0SBen Roth  NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl;
1373b6a24fbSTomasz Sapeta
1383b6a24fbSTomasz Sapeta  // Unregister visible app record so all modules get destroyed.
1393b6a24fbSTomasz Sapeta  [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp];
1403b6a24fbSTomasz Sapeta
1413b6a24fbSTomasz Sapeta  // Create new app record.
142b987b50fSBen Roth  [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil];
143b987b50fSBen Roth}
144b987b50fSBen Roth
14550661f5cSWill Schurman- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(EXManifestsManifest *)manifest
146f44e163eSBen Roth{
147f44e163eSBen Roth  [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest];
148f44e163eSBen Roth}
149f44e163eSBen Roth
150167fd314SWill Schurman- (void)getHistoryUrlForScopeKey:(NSString *)scopeKey completion:(void (^)(NSString *))completion
151f44e163eSBen Roth{
152167fd314SWill Schurman  return [[self _getHomeAppManager] getHistoryUrlForScopeKey:scopeKey completion:completion];
153f44e163eSBen Roth}
154f44e163eSBen Roth
15566169681SBen Roth- (void)setIsNuxFinished:(BOOL)isFinished
15666169681SBen Roth{
15766169681SBen Roth  [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey];
15866169681SBen Roth  [[NSUserDefaults standardUserDefaults] synchronize];
15966169681SBen Roth}
16066169681SBen Roth
16166169681SBen Roth- (BOOL)isNuxFinished
16266169681SBen Roth{
16366169681SBen Roth  return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey];
16466169681SBen Roth}
16566169681SBen Roth
16666169681SBen Roth- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord
16766169681SBen Roth{
16866169681SBen Roth  // show nux if needed
16966169681SBen Roth  if (!self.isNuxFinished
17066169681SBen Roth      && appRecord == [EXKernel sharedInstance].visibleApp
171f67462bcSTomasz Sapeta      && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord) {
172f67462bcSTomasz Sapeta    [[EXDevMenuManager sharedInstance] open];
17366169681SBen Roth  }
174a859e3aeSWojciech Dróżdż
175a859e3aeSWojciech Dróżdż  // Re-apply the default orientation after the app has been loaded (eq. after a reload)
176a859e3aeSWojciech Dróżdż  [self _applySupportedInterfaceOrientations];
17766169681SBen Roth}
17866169681SBen Roth
179b987b50fSBen Roth#pragma mark - internal
180b987b50fSBen Roth
1817f6367d0SBen Roth- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord
1827f6367d0SBen Roth{
183c8452ff7SBartłomiej Bukowski  // Some transition is in progress
184edc8619eSBen Roth  if (_isAnimatingAppTransition) {
185edc8619eSBen Roth    return;
1867f6367d0SBen Roth  }
187c8452ff7SBartłomiej Bukowski
188edc8619eSBen Roth  EXAppViewController *viewControllerToShow = appRecord.viewController;
189a859e3aeSWojciech Dróżdż  _transitioningToViewController = viewControllerToShow;
190c8452ff7SBartłomiej Bukowski
191a859e3aeSWojciech Dróżdż  // Tried to foreground the very same view controller
192c8452ff7SBartłomiej Bukowski  if (viewControllerToShow == self.contentViewController) {
193c8452ff7SBartłomiej Bukowski    return;
1947f6367d0SBen Roth  }
1957f6367d0SBen Roth
196c8452ff7SBartłomiej Bukowski  _isAnimatingAppTransition = YES;
197c8452ff7SBartłomiej Bukowski
198c8452ff7SBartłomiej Bukowski  EXAppViewController *viewControllerToHide = (EXAppViewController *)self.contentViewController;
199c8452ff7SBartłomiej Bukowski
200c8452ff7SBartłomiej Bukowski  if (viewControllerToShow) {
201c8452ff7SBartłomiej Bukowski    [self.view addSubview:viewControllerToShow.view];
202c8452ff7SBartłomiej Bukowski    [self addChildViewController:viewControllerToShow];
203c8452ff7SBartłomiej Bukowski  }
204c8452ff7SBartłomiej Bukowski
205a859e3aeSWojciech Dróżdż  // Try transitioning to the interface orientation of the app before it is shown for smoother transitions
206a859e3aeSWojciech Dróżdż  [self _applySupportedInterfaceOrientations];
207a859e3aeSWojciech Dróżdż
208c8452ff7SBartłomiej Bukowski  EX_WEAKIFY(self)
209c8452ff7SBartłomiej Bukowski  void (^finalizeTransition)(void) = ^{
210c8452ff7SBartłomiej Bukowski    EX_ENSURE_STRONGIFY(self)
211edc8619eSBen Roth    if (viewControllerToHide) {
212ce63205bSTomasz Sapeta      // backgrounds and then dismisses all modals that are presented by the app
213ce63205bSTomasz Sapeta      [viewControllerToHide backgroundControllers];
214d57b5fd1SBrent Vatne      [viewControllerToHide dismissViewControllerAnimated:NO completion:nil];
215edc8619eSBen Roth      [viewControllerToHide willMoveToParentViewController:nil];
216c8452ff7SBartłomiej Bukowski      [viewControllerToHide removeFromParentViewController];
217edc8619eSBen Roth      [viewControllerToHide.view removeFromSuperview];
218e7ac6e4fSBen Roth    }
219c8452ff7SBartłomiej Bukowski
220edc8619eSBen Roth    if (viewControllerToShow) {
221c8452ff7SBartłomiej Bukowski      [viewControllerToShow didMoveToParentViewController:self];
222c8452ff7SBartłomiej Bukowski      self.contentViewController = viewControllerToShow;
223edc8619eSBen Roth    }
224c8452ff7SBartłomiej Bukowski
225c8452ff7SBartłomiej Bukowski    [self.view setNeedsLayout];
226c8452ff7SBartłomiej Bukowski    self.isAnimatingAppTransition = NO;
227a859e3aeSWojciech Dróżdż    self.transitioningToViewController = nil;
228c8452ff7SBartłomiej Bukowski    if (self.delegate) {
229c8452ff7SBartłomiej Bukowski      [self.delegate viewController:self didNavigateAppToVisible:appRecord];
230edc8619eSBen Roth    }
231a859e3aeSWojciech Dróżdż    [self _applySupportedInterfaceOrientations];
232edc8619eSBen Roth  };
233edc8619eSBen Roth
234edc8619eSBen Roth  BOOL animated = (viewControllerToHide && viewControllerToShow);
235c8452ff7SBartłomiej Bukowski  if (!animated) {
236c8452ff7SBartłomiej Bukowski    return finalizeTransition();
237c8452ff7SBartłomiej Bukowski  }
238c8452ff7SBartłomiej Bukowski
239edc8619eSBen Roth  if (viewControllerToHide.contentView) {
240edc8619eSBen Roth    viewControllerToHide.contentView.transform = CGAffineTransformIdentity;
241edc8619eSBen Roth    viewControllerToHide.contentView.alpha = 1.0f;
242edc8619eSBen Roth  }
243edc8619eSBen Roth  if (viewControllerToShow.contentView) {
244edc8619eSBen Roth    viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
245edc8619eSBen Roth    viewControllerToShow.contentView.alpha = 0;
246edc8619eSBen Roth  }
247c8452ff7SBartłomiej Bukowski
248edc8619eSBen Roth  [UIView animateWithDuration:0.3f animations:^{
249edc8619eSBen Roth    if (viewControllerToHide.contentView) {
250edc8619eSBen Roth      viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f);
251edc8619eSBen Roth      viewControllerToHide.contentView.alpha = 0.5f;
252edc8619eSBen Roth    }
253edc8619eSBen Roth    if (viewControllerToShow.contentView) {
254edc8619eSBen Roth      viewControllerToShow.contentView.transform = CGAffineTransformIdentity;
255edc8619eSBen Roth      viewControllerToShow.contentView.alpha = 1.0f;
256edc8619eSBen Roth    }
257edc8619eSBen Roth  } completion:^(BOOL finished) {
258c8452ff7SBartłomiej Bukowski    finalizeTransition();
259edc8619eSBen Roth  }];
260edc8619eSBen Roth}
261edc8619eSBen Roth
262f44e163eSBen Roth- (EXHomeAppManager *)_getHomeAppManager
263f44e163eSBen Roth{
264f44e163eSBen Roth  return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager;
265f44e163eSBen Roth}
266f44e163eSBen Roth
26766169681SBen Roth- (void)_maybeResetNuxState
26866169681SBen Roth{
26966169681SBen Roth  // used by appetize: optionally disable nux
27066169681SBen Roth  BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey];
27166169681SBen Roth  if (disableNuxDefaultsValue) {
27266169681SBen Roth    [self setIsNuxFinished:YES];
27366169681SBen Roth    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey];
27466169681SBen Roth  }
27566169681SBen Roth}
27666169681SBen Roth
277a859e3aeSWojciech Dróżdż- (void)_applySupportedInterfaceOrientations
278a859e3aeSWojciech Dróżdż{
279a859e3aeSWojciech Dróżdż  if (@available(iOS 16, *)) {
280a859e3aeSWojciech Dróżdż    [self setNeedsUpdateOfSupportedInterfaceOrientations];
281a859e3aeSWojciech Dróżdż  } else {
282a859e3aeSWojciech Dróżdż    // On iOS < 16 we need to try to rotate to the desired orientation, which also
283a859e3aeSWojciech Dróżdż    // makes the view controller to update the supported orientations
284a859e3aeSWojciech Dróżdż    UIInterfaceOrientationMask orientationMask = [self supportedInterfaceOrientations];
285a859e3aeSWojciech Dróżdż    [ScreenOrientationRegistry.shared enforceDesiredDeviceOrientationWithOrientationMask:orientationMask];
286a859e3aeSWojciech Dróżdż  }
287a859e3aeSWojciech Dróżdż}
288a859e3aeSWojciech Dróżdż
2897f6367d0SBen Roth@end
2907f6367d0SBen Roth
2917f6367d0SBen RothNS_ASSUME_NONNULL_END
292