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