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