xref: /expo/ios/Client/EXRootViewController.m (revision 19a0af8d)
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