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