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