xref: /expo/ios/Client/EXRootViewController.m (revision 83d491c0)
1// Copyright 2015-present 650 Industries. All rights reserved.
2
3@import UIKit;
4
5#import "EXAppDelegate.h"
6#import "EXAppViewController.h"
7#import "EXHomeAppManager.h"
8#import "EXKernel.h"
9#import "EXAppLoader.h"
10#import "EXKernelAppRecord.h"
11#import "EXKernelAppRegistry.h"
12#import "EXKernelLinkingManager.h"
13#import "EXKernelServiceRegistry.h"
14#import "EXMenuViewController.h"
15#import "EXRootViewController.h"
16
17NSString * const kEXHomeDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey";
18NSString * const kEXHomeIsNuxFinishedDefaultsKey = @"EXHomeIsNuxFinishedDefaultsKey";
19
20NS_ASSUME_NONNULL_BEGIN
21
22@interface EXRootViewController () <EXAppBrowserController>
23
24@property (nonatomic, strong) EXMenuViewController *menuViewController;
25@property (nonatomic, assign) BOOL isMenuVisible;
26@property (nonatomic, assign) BOOL isAnimatingAppTransition;
27@property (nonatomic, strong, nullable) NSNumber *orientationBeforeShowingMenu;
28
29@end
30
31@implementation EXRootViewController
32
33- (instancetype)init
34{
35  if (self = [super init]) {
36    [EXKernel sharedInstance].browserController = self;
37    [self _maybeResetNuxState];
38  }
39  return self;
40}
41
42/**
43 * Overrides UIViewController's method that returns interface orientations that the view controller supports.
44 * If EXMenuViewController is currently shown we want to use its supported orientations so the UI rotates
45 * when we open the dev menu while in the unsupported orientation.
46 * Otherwise, returns interface orientations supported by the current experience.
47 */
48- (UIInterfaceOrientationMask)supportedInterfaceOrientations
49{
50  return _isMenuVisible ? [_menuViewController supportedInterfaceOrientations] : [self.contentViewController supportedInterfaceOrientations];
51}
52
53/**
54 * Same case as above with `supportedInterfaceOrientations` method.
55 * If we don't override this, we can get incorrect orientation while changing device orientation when the dev menu is visible.
56 */
57- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
58{
59  return _isMenuVisible ? [_menuViewController preferredInterfaceOrientationForPresentation] : [self.contentViewController preferredInterfaceOrientationForPresentation];
60}
61
62#pragma mark - EXViewController
63
64- (void)createRootAppAndMakeVisible
65{
66  EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init];
67  EXAppLoader *homeAppLoader = [[EXAppLoader 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)toggleMenuWithCompletion:(void (^ _Nullable)(void))completion
87{
88  [self setIsMenuVisible:!_isMenuVisible completion:completion];
89}
90
91/**
92 * Sets the visibility of the dev menu and attempts to rotate the UI according to interface orientations supported by the view controller that is on top.
93 */
94- (void)setIsMenuVisible:(BOOL)isMenuVisible completion:(void (^ _Nullable)(void))completion
95{
96  if (!_menuViewController) {
97    _menuViewController = [[EXMenuViewController alloc] init];
98  }
99  if (isMenuVisible != _isMenuVisible) {
100    _isMenuVisible = isMenuVisible;
101
102    if (isMenuVisible) {
103      // We need to force the device to use portrait orientation as the dev menu doesn't support landscape.
104      // However, when removing it, we should set it back to the orientation from before showing the dev menu.
105      _orientationBeforeShowingMenu = [[UIDevice currentDevice] valueForKey:@"orientation"];
106      [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait) forKey:@"orientation"];
107    } else {
108      // Restore the original orientation that had been set before the dev menu was displayed.
109      [[UIDevice currentDevice] setValue:_orientationBeforeShowingMenu forKey:@"orientation"];
110    }
111
112    // Ask the system to rotate the UI to device orientation that we've just set to fake value (see previous line of code).
113    [UIViewController attemptRotationToDeviceOrientation];
114
115    if (isMenuVisible) {
116      // Add menu view controller as a child of the root view controller.
117      [_menuViewController willMoveToParentViewController:self];
118      [_menuViewController.view setFrame:self.view.frame];
119      [self.view addSubview:_menuViewController.view];
120      [_menuViewController didMoveToParentViewController:self];
121    } else {
122      // Detach menu view controller from the root view controller.
123      [_menuViewController willMoveToParentViewController:nil];
124      [_menuViewController.view removeFromSuperview];
125      [_menuViewController didMoveToParentViewController:nil];
126    }
127  }
128  if (completion) {
129    completion();
130  }
131}
132
133- (BOOL)isMenuVisible
134{
135  return _isMenuVisible;
136}
137
138- (void)showQRReader
139{
140  [self moveHomeToVisible];
141  [[self _getHomeAppManager] showQRReader];
142}
143
144- (void)moveHomeToVisible
145{
146  __weak typeof(self) weakSelf = self;
147  [self setIsMenuVisible:NO completion:^{
148    __strong typeof(weakSelf) strongSelf = weakSelf;
149    if (strongSelf) {
150      [strongSelf moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord];
151
152      if (strongSelf.isMenuVisible) {
153        [strongSelf setIsMenuVisible:NO completion:nil];
154      }
155    }
156  }];
157}
158
159// this is different from Util.reload()
160// because it can work even on an errored app record (e.g. with no manifest, or with no running bridge).
161- (void)reloadVisibleApp
162{
163  if (_isMenuVisible) {
164    [self setIsMenuVisible:NO completion:nil];
165  }
166
167  EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp;
168  [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp];
169  NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl;
170
171  // Unregister visible app record so all modules get destroyed.
172  [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp];
173
174  // Create new app record.
175  [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil];
176}
177
178- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(NSDictionary *)manifest
179{
180  [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest];
181}
182
183- (void)getHistoryUrlForExperienceId:(NSString *)experienceId completion:(void (^)(NSString *))completion
184{
185  return [[self _getHomeAppManager] getHistoryUrlForExperienceId:experienceId completion:completion];
186}
187
188- (void)setIsNuxFinished:(BOOL)isFinished
189{
190  [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey];
191  [[NSUserDefaults standardUserDefaults] synchronize];
192}
193
194- (BOOL)isNuxFinished
195{
196  return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey];
197}
198
199- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord
200{
201  // show nux if needed
202  if (!self.isNuxFinished
203      && appRecord == [EXKernel sharedInstance].visibleApp
204      && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord
205      && !self.isMenuVisible) {
206    [self setIsMenuVisible:YES completion:nil];
207  }
208}
209
210#pragma mark - internal
211
212- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord
213{
214  if (_isAnimatingAppTransition) {
215    return;
216  }
217  EXAppViewController *viewControllerToShow = appRecord.viewController;
218  EXAppViewController *viewControllerToHide;
219  if (viewControllerToShow != self.contentViewController) {
220    _isAnimatingAppTransition = YES;
221    if (self.contentViewController) {
222      viewControllerToHide = (EXAppViewController *)self.contentViewController;
223    }
224    if (viewControllerToShow) {
225      [viewControllerToShow willMoveToParentViewController:self];
226      [self.view addSubview:viewControllerToShow.view];
227      [viewControllerToShow foregroundControllers];
228    }
229
230    __weak typeof(self) weakSelf = self;
231    void (^transitionFinished)(void) = ^{
232      __strong typeof(weakSelf) strongSelf = weakSelf;
233      if (strongSelf) {
234        if (viewControllerToHide) {
235          // backgrounds and then dismisses all modals that are presented by the app
236          [viewControllerToHide backgroundControllers];
237          [viewControllerToHide dismissViewControllerAnimated:NO completion:nil];
238          [viewControllerToHide willMoveToParentViewController:nil];
239          [viewControllerToHide.view removeFromSuperview];
240          [viewControllerToHide didMoveToParentViewController:nil];
241        }
242        if (viewControllerToShow) {
243          [viewControllerToShow didMoveToParentViewController:strongSelf];
244          strongSelf.contentViewController = viewControllerToShow;
245        }
246        [strongSelf.view setNeedsLayout];
247        strongSelf.isAnimatingAppTransition = NO;
248        if (strongSelf.delegate) {
249          [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord];
250        }
251      }
252    };
253
254    BOOL animated = (viewControllerToHide && viewControllerToShow);
255    if (animated) {
256      if (viewControllerToHide.contentView) {
257        viewControllerToHide.contentView.transform = CGAffineTransformIdentity;
258        viewControllerToHide.contentView.alpha = 1.0f;
259      }
260      if (viewControllerToShow.contentView) {
261        viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
262        viewControllerToShow.contentView.alpha = 0;
263      }
264      [UIView animateWithDuration:0.3f animations:^{
265        if (viewControllerToHide.contentView) {
266          viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f);
267          viewControllerToHide.contentView.alpha = 0.5f;
268        }
269        if (viewControllerToShow.contentView) {
270          viewControllerToShow.contentView.transform = CGAffineTransformIdentity;
271          viewControllerToShow.contentView.alpha = 1.0f;
272        }
273      } completion:^(BOOL finished) {
274        transitionFinished();
275      }];
276    } else {
277      transitionFinished();
278    }
279  }
280}
281
282- (EXHomeAppManager *)_getHomeAppManager
283{
284  return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager;
285}
286
287- (void)_maybeResetNuxState
288{
289  // used by appetize: optionally disable nux
290  BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey];
291  if (disableNuxDefaultsValue) {
292    [self setIsNuxFinished:YES];
293    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey];
294  }
295}
296
297@end
298
299NS_ASSUME_NONNULL_END
300