xref: /expo/ios/Client/EXRootViewController.m (revision 23cc0e46)
1// Copyright 2015-present 650 Industries. All rights reserved.
2
3@import UIKit;
4
5#import "EXAppDelegate.h"
6#import "EXAppViewController.h"
7#import "EXButtonView.h"
8#import "EXHomeAppManager.h"
9#import "EXHomeDiagnosticsViewController.h"
10#import "EXKernel.h"
11#import "EXAppLoader.h"
12#import "EXKernelAppRecord.h"
13#import "EXKernelAppRegistry.h"
14#import "EXKernelDevKeyCommands.h"
15#import "EXKernelLinkingManager.h"
16#import "EXKernelServiceRegistry.h"
17#import "EXMenuGestureRecognizer.h"
18#import "EXMenuViewController.h"
19#import "EXRootViewController.h"
20#import "EXMenuWindow.h"
21
22NSString * const kEXHomeDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey";
23NSString * const kEXHomeIsNuxFinishedDefaultsKey = @"EXHomeIsNuxFinishedDefaultsKey";
24
25NS_ASSUME_NONNULL_BEGIN
26
27@interface EXRootViewController () <EXAppBrowserController>
28
29@property (nonatomic, strong) EXMenuViewController *menuViewController;
30@property (nonatomic, assign) BOOL isMenuVisible;
31@property (nonatomic, assign) BOOL isAnimatingMenu;
32@property (nonatomic, assign) BOOL isAnimatingAppTransition;
33@property (nonatomic, strong) EXButtonView *btnMenu;
34@property (nonatomic, strong, nullable) EXMenuWindow *menuWindow;
35
36@end
37
38@implementation EXRootViewController
39
40- (instancetype)init
41{
42  if (self = [super init]) {
43    [EXKernel sharedInstance].browserController = self;
44    [[NSNotificationCenter defaultCenter] addObserver:self
45                                             selector:@selector(_updateMenuButtonBehavior)
46                                                 name:kEXKernelDidChangeMenuBehaviorNotification
47                                               object:nil];
48    [self _maybeResetNuxState];
49  }
50  return self;
51}
52
53- (void)viewDidLoad
54{
55  [super viewDidLoad];
56  _btnMenu = [[EXButtonView alloc] init];
57  _btnMenu.hidden = YES;
58  [self.view addSubview:_btnMenu];
59  EXMenuGestureRecognizer *menuGestureRecognizer = [[EXMenuGestureRecognizer alloc] initWithTarget:self action:@selector(_onMenuGestureRecognized:)];
60  [((EXAppDelegate *)[UIApplication sharedApplication].delegate).window addGestureRecognizer:menuGestureRecognizer];
61}
62
63- (void)viewWillLayoutSubviews
64{
65  [super viewWillLayoutSubviews];
66  _btnMenu.frame = CGRectMake(0, 0, 48.0f, 48.0f);
67  _btnMenu.center = CGPointMake(self.view.frame.size.width - 36.0f, self.view.frame.size.height - 72.0f);
68  [self.view bringSubviewToFront:_btnMenu];
69}
70
71#pragma mark - EXViewController
72
73- (void)createRootAppAndMakeVisible
74{
75  EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init];
76  EXAppLoader *homeAppLoader = [[EXAppLoader alloc] initWithLocalManifest:[EXHomeAppManager bundledHomeManifest]];
77  EXKernelAppRecord *homeAppRecord = [[EXKernelAppRecord alloc] initWithAppLoader:homeAppLoader appManager:homeAppManager];
78  [[EXKernel sharedInstance].appRegistry registerHomeAppRecord:homeAppRecord];
79  [self moveAppToVisible:homeAppRecord];
80}
81
82#pragma mark - EXAppBrowserController
83
84- (void)moveAppToVisible:(EXKernelAppRecord *)appRecord
85{
86  [self _foregroundAppRecord:appRecord];
87
88  // When foregrounding the app record we want to add it to the history to handle the edge case
89  // where a user opened a project, then went to home and cleared history, then went back to a
90  // the already open project.
91  [self addHistoryItemWithUrl:appRecord.appLoader.manifestUrl manifest:appRecord.appLoader.manifest];
92
93}
94
95- (void)toggleMenuWithCompletion:(void (^ _Nullable)(void))completion
96{
97  [self setIsMenuVisible:!_isMenuVisible completion:completion];
98}
99
100- (void)setIsMenuVisible:(BOOL)isMenuVisible completion:(void (^ _Nullable)(void))completion
101{
102  if (!_menuViewController) {
103    _menuViewController = [[EXMenuViewController alloc] init];
104  }
105  if (isMenuVisible != _isMenuVisible) {
106    if (!_isAnimatingMenu) {
107      _isMenuVisible = isMenuVisible;
108      [self _animateMenuToVisible:_isMenuVisible completion:completion];
109    }
110  } else {
111    completion();
112  }
113}
114
115- (void)showDiagnostics
116{
117  __weak typeof(self) weakSelf = self;
118  [self setIsMenuVisible:NO completion:^{
119    __strong typeof(weakSelf) strongSelf = weakSelf;
120    if (strongSelf) {
121      EXHomeDiagnosticsViewController *vcDiagnostics = [[EXHomeDiagnosticsViewController alloc] init];
122      [strongSelf presentViewController:vcDiagnostics animated:NO completion:nil];
123    }
124  }];
125}
126
127- (void)showQRReader
128{
129  [self moveHomeToVisible];
130  [[self _getHomeAppManager] showQRReader];
131}
132
133- (void)moveHomeToVisible
134{
135  __weak typeof(self) weakSelf = self;
136  [self setIsMenuVisible:NO completion:^{
137    __strong typeof(weakSelf) strongSelf = weakSelf;
138    if (strongSelf) {
139      [strongSelf moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord];
140
141      if (strongSelf.isMenuVisible) {
142        [strongSelf setIsMenuVisible:NO completion:nil];
143      }
144    }
145  }];
146}
147
148// this is different from Util.reload()
149// because it can work even on an errored app record (e.g. with no manifest, or with no running bridge).
150- (void)reloadVisibleApp
151{
152  if (_isMenuVisible) {
153    [self setIsMenuVisible:NO completion:nil];
154  }
155
156  EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp;
157  [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp];
158  NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl;
159
160  // Unregister visible app record so all modules get destroyed.
161  [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp];
162
163  // Create new app record.
164  [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil];
165}
166
167- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(NSDictionary *)manifest
168{
169  [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest];
170}
171
172- (void)getHistoryUrlForExperienceId:(NSString *)experienceId completion:(void (^)(NSString *))completion
173{
174  return [[self _getHomeAppManager] getHistoryUrlForExperienceId:experienceId completion:completion];
175}
176
177- (void)setIsNuxFinished:(BOOL)isFinished
178{
179  [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey];
180  [[NSUserDefaults standardUserDefaults] synchronize];
181}
182
183- (BOOL)isNuxFinished
184{
185  return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey];
186}
187
188- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord
189{
190  // show nux if needed
191  if (!self.isNuxFinished
192      && appRecord == [EXKernel sharedInstance].visibleApp
193      && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord
194      && !self.isMenuVisible) {
195    [self setIsMenuVisible:YES completion:nil];
196  }
197
198  // check button availability when any new app loads
199  [self _updateMenuButtonBehavior];
200}
201
202#pragma mark - internal
203
204- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord
205{
206  if (_isAnimatingAppTransition) {
207    return;
208  }
209  EXAppViewController *viewControllerToShow = appRecord.viewController;
210  EXAppViewController *viewControllerToHide;
211  if (viewControllerToShow != self.contentViewController) {
212    _isAnimatingAppTransition = YES;
213    if (self.contentViewController) {
214      viewControllerToHide = (EXAppViewController *)self.contentViewController;
215    }
216    if (viewControllerToShow) {
217      [viewControllerToShow willMoveToParentViewController:self];
218      [self.view addSubview:viewControllerToShow.view];
219      [viewControllerToShow foregroundControllers];
220    }
221
222    __weak typeof(self) weakSelf = self;
223    void (^transitionFinished)(void) = ^{
224      __strong typeof(weakSelf) strongSelf = weakSelf;
225      if (strongSelf) {
226        if (viewControllerToHide) {
227          // backgrounds and then dismisses all modals that are presented by the app
228          [viewControllerToHide backgroundControllers];
229          [viewControllerToHide dismissViewControllerAnimated:NO completion:nil];
230          [viewControllerToHide willMoveToParentViewController:nil];
231          [viewControllerToHide.view removeFromSuperview];
232          [viewControllerToHide didMoveToParentViewController:nil];
233        }
234        if (viewControllerToShow) {
235          [viewControllerToShow didMoveToParentViewController:strongSelf];
236          strongSelf.contentViewController = viewControllerToShow;
237        }
238        [strongSelf.view setNeedsLayout];
239        strongSelf.isAnimatingAppTransition = NO;
240        if (strongSelf.delegate) {
241          [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord];
242        }
243      }
244    };
245
246    BOOL animated = (viewControllerToHide && viewControllerToShow);
247    if (animated) {
248      if (viewControllerToHide.contentView) {
249        viewControllerToHide.contentView.transform = CGAffineTransformIdentity;
250        viewControllerToHide.contentView.alpha = 1.0f;
251      }
252      if (viewControllerToShow.contentView) {
253        viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
254        viewControllerToShow.contentView.alpha = 0;
255      }
256      [UIView animateWithDuration:0.3f animations:^{
257        if (viewControllerToHide.contentView) {
258          viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f);
259          viewControllerToHide.contentView.alpha = 0.5f;
260        }
261        if (viewControllerToShow.contentView) {
262          viewControllerToShow.contentView.transform = CGAffineTransformIdentity;
263          viewControllerToShow.contentView.alpha = 1.0f;
264        }
265      } completion:^(BOOL finished) {
266        transitionFinished();
267      }];
268    } else {
269      transitionFinished();
270    }
271  }
272}
273
274- (void)_animateMenuToVisible:(BOOL)visible completion:(void (^ _Nullable)(void))completion
275{
276  _isAnimatingMenu = YES;
277  __weak typeof(self) weakSelf = self;
278  if (visible) {
279    [_menuViewController willMoveToParentViewController:self];
280
281    if (_menuWindow == nil) {
282      _menuWindow = [[EXMenuWindow alloc] init];
283    }
284
285    [_menuWindow setFrame:self.view.frame];
286    [_menuWindow addSubview:_menuViewController.view];
287    [_menuWindow makeKeyAndVisible];
288
289    _menuViewController.view.alpha = 0.0f;
290    _menuViewController.view.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
291    [UIView animateWithDuration:0.1f animations:^{
292      self.menuViewController.view.alpha = 1.0f;
293      self.menuViewController.view.transform = CGAffineTransformIdentity;
294    } completion:^(BOOL finished) {
295      __strong typeof(weakSelf) strongSelf = weakSelf;
296      if (strongSelf) {
297        strongSelf.isAnimatingMenu = NO;
298        [strongSelf.menuViewController didMoveToParentViewController:self];
299        if (completion) {
300          completion();
301        }
302      }
303    }];
304  } else {
305    _menuViewController.view.alpha = 1.0f;
306    [UIView animateWithDuration:0.1f animations:^{
307      self.menuViewController.view.alpha = 0.0f;
308    } completion:^(BOOL finished) {
309      __strong typeof(weakSelf) strongSelf = weakSelf;
310      if (strongSelf) {
311        strongSelf.isAnimatingMenu = NO;
312        [strongSelf.menuViewController willMoveToParentViewController:nil];
313        [strongSelf.menuViewController.view removeFromSuperview];
314        [strongSelf.menuViewController didMoveToParentViewController:nil];
315        strongSelf.menuWindow = nil;
316        if (completion) {
317          completion();
318        }
319      }
320    }];
321  }
322}
323
324- (EXHomeAppManager *)_getHomeAppManager
325{
326  return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager;
327}
328
329- (void)_maybeResetNuxState
330{
331  // used by appetize: optionally disable nux
332  BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey];
333  if (disableNuxDefaultsValue) {
334    [self setIsNuxFinished:YES];
335    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey];
336  }
337}
338
339- (void)_updateMenuButtonBehavior
340{
341  BOOL shouldShowButton = [[EXKernelDevKeyCommands sharedInstance] isLegacyMenuButtonAvailable];
342  dispatch_async(dispatch_get_main_queue(), ^{
343    self.btnMenu.hidden = !shouldShowButton;
344  });
345}
346
347- (void)_onMenuGestureRecognized:(EXMenuGestureRecognizer *)sender
348{
349  if (sender.state == UIGestureRecognizerStateEnded) {
350    [[EXKernel sharedInstance] switchTasks];
351  }
352}
353
354@end
355
356NS_ASSUME_NONNULL_END
357