xref: /expo/ios/Client/EXRootViewController.m (revision 4e9980b6)
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 (!_isAnimatingMenu && isMenuVisible != _isMenuVisible) {
105    _isMenuVisible = isMenuVisible;
106    [self _animateMenuToVisible:_isMenuVisible completion:completion];
107  }
108}
109
110- (void)showDiagnostics
111{
112  __weak typeof(self) weakSelf = self;
113  [self setIsMenuVisible:NO completion:^{
114    __strong typeof(weakSelf) strongSelf = weakSelf;
115    if (strongSelf) {
116      EXHomeDiagnosticsViewController *vcDiagnostics = [[EXHomeDiagnosticsViewController alloc] init];
117      [strongSelf presentViewController:vcDiagnostics animated:NO completion:nil];
118    }
119  }];
120}
121
122- (void)showQRReader
123{
124  [self moveHomeToVisible];
125  [[self _getHomeAppManager] showQRReader];
126}
127
128- (void)moveHomeToVisible
129{
130  __weak typeof(self) weakSelf = self;
131  [self setIsMenuVisible:NO completion:^{
132    __strong typeof(weakSelf) strongSelf = weakSelf;
133    if (strongSelf) {
134      [strongSelf moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord];
135    }
136  }];
137}
138
139- (void)refreshVisibleApp
140{
141  // this is different from Util.reload()
142  // because it can work even on an errored app record (e.g. with no manifest, or with no running bridge).
143  [self setIsMenuVisible:NO completion:nil];
144  EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp;
145  [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp];
146  NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl;
147  [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil];
148}
149
150- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(NSDictionary *)manifest
151{
152  [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest];
153}
154
155- (void)getHistoryUrlForExperienceId:(NSString *)experienceId completion:(void (^)(NSString *))completion
156{
157  return [[self _getHomeAppManager] getHistoryUrlForExperienceId:experienceId completion:completion];
158}
159
160- (void)setIsNuxFinished:(BOOL)isFinished
161{
162  [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey];
163  [[NSUserDefaults standardUserDefaults] synchronize];
164}
165
166- (BOOL)isNuxFinished
167{
168  return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey];
169}
170
171- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord
172{
173  // show nux if needed
174  if (!self.isNuxFinished
175      && appRecord == [EXKernel sharedInstance].visibleApp
176      && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord
177      && !self.isMenuVisible) {
178    [self setIsMenuVisible:YES completion:nil];
179  }
180
181  // check button availability when any new app loads
182  [self _updateMenuButtonBehavior];
183}
184
185#pragma mark - internal
186
187- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord
188{
189  if (_isAnimatingAppTransition) {
190    return;
191  }
192  EXAppViewController *viewControllerToShow = appRecord.viewController;
193  EXAppViewController *viewControllerToHide;
194  if (viewControllerToShow != self.contentViewController) {
195    _isAnimatingAppTransition = YES;
196    if (self.contentViewController) {
197      viewControllerToHide = (EXAppViewController *)self.contentViewController;
198    }
199    if (viewControllerToShow) {
200      [viewControllerToShow willMoveToParentViewController:self];
201      [self.view addSubview:viewControllerToShow.view];
202    }
203
204    __weak typeof(self) weakSelf = self;
205    void (^transitionFinished)(void) = ^{
206      __strong typeof(weakSelf) strongSelf = weakSelf;
207      if (strongSelf) {
208        if (viewControllerToHide) {
209          [viewControllerToHide willMoveToParentViewController:nil];
210          [viewControllerToHide.view removeFromSuperview];
211          [viewControllerToHide didMoveToParentViewController:nil];
212        }
213        if (viewControllerToShow) {
214          [viewControllerToShow didMoveToParentViewController:strongSelf];
215          strongSelf.contentViewController = viewControllerToShow;
216        }
217        [strongSelf.view setNeedsLayout];
218        strongSelf.isAnimatingAppTransition = NO;
219        if (strongSelf.delegate) {
220          [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord];
221        }
222      }
223    };
224
225    BOOL animated = (viewControllerToHide && viewControllerToShow);
226    if (animated) {
227      if (viewControllerToHide.contentView) {
228        viewControllerToHide.contentView.transform = CGAffineTransformIdentity;
229        viewControllerToHide.contentView.alpha = 1.0f;
230      }
231      if (viewControllerToShow.contentView) {
232        viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
233        viewControllerToShow.contentView.alpha = 0;
234      }
235      [UIView animateWithDuration:0.3f animations:^{
236        if (viewControllerToHide.contentView) {
237          viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f);
238          viewControllerToHide.contentView.alpha = 0.5f;
239        }
240        if (viewControllerToShow.contentView) {
241          viewControllerToShow.contentView.transform = CGAffineTransformIdentity;
242          viewControllerToShow.contentView.alpha = 1.0f;
243        }
244      } completion:^(BOOL finished) {
245        transitionFinished();
246      }];
247    } else {
248      transitionFinished();
249    }
250  }
251}
252
253- (void)_animateMenuToVisible:(BOOL)visible completion:(void (^ _Nullable)(void))completion
254{
255  _isAnimatingMenu = YES;
256  __weak typeof(self) weakSelf = self;
257  if (visible) {
258    [_menuViewController willMoveToParentViewController:self];
259    [self.view addSubview:_menuViewController.view];
260    _menuViewController.view.alpha = 0.0f;
261    _menuViewController.view.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
262    [UIView animateWithDuration:0.1f animations:^{
263      _menuViewController.view.alpha = 1.0f;
264      _menuViewController.view.transform = CGAffineTransformIdentity;
265    } completion:^(BOOL finished) {
266      __strong typeof(weakSelf) strongSelf = weakSelf;
267      if (strongSelf) {
268        strongSelf.isAnimatingMenu = NO;
269        [strongSelf.menuViewController didMoveToParentViewController:self];
270        if (completion) {
271          completion();
272        }
273      }
274    }];
275  } else {
276    _menuViewController.view.alpha = 1.0f;
277    [UIView animateWithDuration:0.1f animations:^{
278      _menuViewController.view.alpha = 0.0f;
279    } completion:^(BOOL finished) {
280      __strong typeof(weakSelf) strongSelf = weakSelf;
281      if (strongSelf) {
282        strongSelf.isAnimatingMenu = NO;
283        [strongSelf.menuViewController willMoveToParentViewController:nil];
284        [strongSelf.menuViewController.view removeFromSuperview];
285        [strongSelf.menuViewController didMoveToParentViewController:nil];
286        if (completion) {
287          completion();
288        }
289      }
290    }];
291  }
292}
293
294- (EXHomeAppManager *)_getHomeAppManager
295{
296  return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager;
297}
298
299- (void)_maybeResetNuxState
300{
301  // used by appetize: optionally disable nux
302  BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey];
303  if (disableNuxDefaultsValue) {
304    [self setIsNuxFinished:YES];
305    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey];
306  }
307}
308
309- (void)_updateMenuButtonBehavior
310{
311  BOOL shouldShowButton = [[EXKernelDevKeyCommands sharedInstance] isLegacyMenuButtonAvailable];
312  dispatch_async(dispatch_get_main_queue(), ^{
313    _btnMenu.hidden = !shouldShowButton;
314  });
315}
316
317- (void)_onMenuGestureRecognized:(EXMenuGestureRecognizer *)sender
318{
319  if (sender.state == UIGestureRecognizerStateEnded) {
320    [[EXKernel sharedInstance] switchTasks];
321  }
322}
323
324@end
325
326NS_ASSUME_NONNULL_END
327