xref: /expo/ios/Client/EXRootViewController.m (revision f2369006)
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] initWithLocalManifest:[EXHomeAppManager bundledHomeManifest]];
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