xref: /expo/ios/Client/EXRootViewController.m (revision e31fcd00)
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  [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp];
117  NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl;
118
119  // Unregister visible app record so all modules get destroyed.
120  [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp];
121
122  // Create new app record.
123  [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil];
124}
125
126- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(EXManifestsManifest *)manifest
127{
128  [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest];
129}
130
131- (void)getHistoryUrlForScopeKey:(NSString *)scopeKey completion:(void (^)(NSString *))completion
132{
133  return [[self _getHomeAppManager] getHistoryUrlForScopeKey:scopeKey completion:completion];
134}
135
136- (void)setIsNuxFinished:(BOOL)isFinished
137{
138  [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey];
139  [[NSUserDefaults standardUserDefaults] synchronize];
140}
141
142- (BOOL)isNuxFinished
143{
144  return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey];
145}
146
147- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord
148{
149  // show nux if needed
150  if (!self.isNuxFinished
151      && appRecord == [EXKernel sharedInstance].visibleApp
152      && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord) {
153    [[EXDevMenuManager sharedInstance] open];
154  }
155}
156
157#pragma mark - internal
158
159- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord
160{
161  // Some transition is in progress
162  if (_isAnimatingAppTransition) {
163    return;
164  }
165
166  EXAppViewController *viewControllerToShow = appRecord.viewController;
167
168  // Tried to foregroung the very same view controller
169  if (viewControllerToShow == self.contentViewController) {
170    return;
171  }
172
173  _isAnimatingAppTransition = YES;
174
175  EXAppViewController *viewControllerToHide = (EXAppViewController *)self.contentViewController;
176
177  if (viewControllerToShow) {
178    [self.view addSubview:viewControllerToShow.view];
179    [self addChildViewController:viewControllerToShow];
180  }
181
182  EX_WEAKIFY(self)
183  void (^finalizeTransition)(void) = ^{
184    EX_ENSURE_STRONGIFY(self)
185    if (viewControllerToHide) {
186      // backgrounds and then dismisses all modals that are presented by the app
187      [viewControllerToHide backgroundControllers];
188      [viewControllerToHide dismissViewControllerAnimated:NO completion:nil];
189      [viewControllerToHide willMoveToParentViewController:nil];
190      [viewControllerToHide removeFromParentViewController];
191      [viewControllerToHide.view removeFromSuperview];
192    }
193
194    if (viewControllerToShow) {
195      [viewControllerToShow didMoveToParentViewController:self];
196      self.contentViewController = viewControllerToShow;
197    }
198
199    [self.view setNeedsLayout];
200    self.isAnimatingAppTransition = NO;
201    if (self.delegate) {
202      [self.delegate viewController:self didNavigateAppToVisible:appRecord];
203    }
204  };
205
206  BOOL animated = (viewControllerToHide && viewControllerToShow);
207  if (!animated) {
208    return finalizeTransition();
209  }
210
211  if (viewControllerToHide.contentView) {
212    viewControllerToHide.contentView.transform = CGAffineTransformIdentity;
213    viewControllerToHide.contentView.alpha = 1.0f;
214  }
215  if (viewControllerToShow.contentView) {
216    viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
217    viewControllerToShow.contentView.alpha = 0;
218  }
219
220  [UIView animateWithDuration:0.3f animations:^{
221    if (viewControllerToHide.contentView) {
222      viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f);
223      viewControllerToHide.contentView.alpha = 0.5f;
224    }
225    if (viewControllerToShow.contentView) {
226      viewControllerToShow.contentView.transform = CGAffineTransformIdentity;
227      viewControllerToShow.contentView.alpha = 1.0f;
228    }
229  } completion:^(BOOL finished) {
230    finalizeTransition();
231  }];
232}
233
234- (EXHomeAppManager *)_getHomeAppManager
235{
236  return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager;
237}
238
239- (void)_maybeResetNuxState
240{
241  // used by appetize: optionally disable nux
242  BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey];
243  if (disableNuxDefaultsValue) {
244    [self setIsNuxFinished:YES];
245    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey];
246  }
247}
248
249@end
250
251NS_ASSUME_NONNULL_END
252