xref: /expo/ios/Client/EXRootViewController.m (revision 427ff67e)
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 "EXKernel.h"
10#import "EXAppLoader.h"
11#import "EXKernelAppRecord.h"
12#import "EXKernelAppRegistry.h"
13#import "EXKernelDevKeyCommands.h"
14#import "EXKernelLinkingManager.h"
15#import "EXKernelServiceRegistry.h"
16#import "EXMenuGestureRecognizer.h"
17#import "EXMenuViewController.h"
18#import "EXRootViewController.h"
19#import "EXMenuWindow.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@property (nonatomic, strong, nullable) EXMenuWindow *menuWindow;
34
35@end
36
37@implementation EXRootViewController
38
39- (instancetype)init
40{
41  if (self = [super init]) {
42    [EXKernel sharedInstance].browserController = self;
43    [[NSNotificationCenter defaultCenter] addObserver:self
44                                             selector:@selector(_updateMenuButtonBehavior)
45                                                 name:kEXKernelDidChangeMenuBehaviorNotification
46                                               object:nil];
47    [self _maybeResetNuxState];
48  }
49  return self;
50}
51
52- (void)viewDidLoad
53{
54  [super viewDidLoad];
55  _btnMenu = [[EXButtonView alloc] init];
56  _btnMenu.hidden = YES;
57  [self.view addSubview:_btnMenu];
58  EXMenuGestureRecognizer *menuGestureRecognizer = [[EXMenuGestureRecognizer alloc] initWithTarget:self action:@selector(_onMenuGestureRecognized:)];
59  [((EXAppDelegate *)[UIApplication sharedApplication].delegate).window addGestureRecognizer:menuGestureRecognizer];
60}
61
62- (void)viewWillLayoutSubviews
63{
64  [super viewWillLayoutSubviews];
65  _btnMenu.frame = CGRectMake(0, 0, 48.0f, 48.0f);
66  _btnMenu.center = CGPointMake(self.view.frame.size.width - 36.0f, self.view.frame.size.height - 72.0f);
67  [self.view bringSubviewToFront:_btnMenu];
68}
69
70#pragma mark - EXViewController
71
72- (void)createRootAppAndMakeVisible
73{
74  EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init];
75  EXAppLoader *homeAppLoader = [[EXAppLoader alloc] initWithLocalManifest:[EXHomeAppManager bundledHomeManifest]];
76  EXKernelAppRecord *homeAppRecord = [[EXKernelAppRecord alloc] initWithAppLoader:homeAppLoader appManager:homeAppManager];
77  [[EXKernel sharedInstance].appRegistry registerHomeAppRecord:homeAppRecord];
78  [self moveAppToVisible:homeAppRecord];
79}
80
81#pragma mark - EXAppBrowserController
82
83- (void)moveAppToVisible:(EXKernelAppRecord *)appRecord
84{
85  [self _foregroundAppRecord:appRecord];
86
87  // When foregrounding the app record we want to add it to the history to handle the edge case
88  // where a user opened a project, then went to home and cleared history, then went back to a
89  // the already open project.
90  [self addHistoryItemWithUrl:appRecord.appLoader.manifestUrl manifest:appRecord.appLoader.manifest];
91
92}
93
94- (void)toggleMenuWithCompletion:(void (^ _Nullable)(void))completion
95{
96  [self setIsMenuVisible:!_isMenuVisible completion:completion];
97}
98
99- (void)setIsMenuVisible:(BOOL)isMenuVisible completion:(void (^ _Nullable)(void))completion
100{
101  if (!_menuViewController) {
102    _menuViewController = [[EXMenuViewController alloc] init];
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)showQRReader
115{
116  [self moveHomeToVisible];
117  [[self _getHomeAppManager] showQRReader];
118}
119
120- (void)moveHomeToVisible
121{
122  __weak typeof(self) weakSelf = self;
123  [self setIsMenuVisible:NO completion:^{
124    __strong typeof(weakSelf) strongSelf = weakSelf;
125    if (strongSelf) {
126      [strongSelf moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord];
127
128      if (strongSelf.isMenuVisible) {
129        [strongSelf setIsMenuVisible:NO completion:nil];
130      }
131    }
132  }];
133}
134
135// this is different from Util.reload()
136// because it can work even on an errored app record (e.g. with no manifest, or with no running bridge).
137- (void)reloadVisibleApp
138{
139  if (_isMenuVisible) {
140    [self setIsMenuVisible:NO completion:nil];
141  }
142
143  EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp;
144  [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp];
145  NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl;
146
147  // Unregister visible app record so all modules get destroyed.
148  [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp];
149
150  // Create new app record.
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      [viewControllerToShow foregroundControllers];
207    }
208
209    __weak typeof(self) weakSelf = self;
210    void (^transitionFinished)(void) = ^{
211      __strong typeof(weakSelf) strongSelf = weakSelf;
212      if (strongSelf) {
213        if (viewControllerToHide) {
214          // backgrounds and then dismisses all modals that are presented by the app
215          [viewControllerToHide backgroundControllers];
216          [viewControllerToHide dismissViewControllerAnimated:NO completion:nil];
217          [viewControllerToHide willMoveToParentViewController:nil];
218          [viewControllerToHide.view removeFromSuperview];
219          [viewControllerToHide didMoveToParentViewController:nil];
220        }
221        if (viewControllerToShow) {
222          [viewControllerToShow didMoveToParentViewController:strongSelf];
223          strongSelf.contentViewController = viewControllerToShow;
224        }
225        [strongSelf.view setNeedsLayout];
226        strongSelf.isAnimatingAppTransition = NO;
227        if (strongSelf.delegate) {
228          [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord];
229        }
230      }
231    };
232
233    BOOL animated = (viewControllerToHide && viewControllerToShow);
234    if (animated) {
235      if (viewControllerToHide.contentView) {
236        viewControllerToHide.contentView.transform = CGAffineTransformIdentity;
237        viewControllerToHide.contentView.alpha = 1.0f;
238      }
239      if (viewControllerToShow.contentView) {
240        viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
241        viewControllerToShow.contentView.alpha = 0;
242      }
243      [UIView animateWithDuration:0.3f animations:^{
244        if (viewControllerToHide.contentView) {
245          viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f);
246          viewControllerToHide.contentView.alpha = 0.5f;
247        }
248        if (viewControllerToShow.contentView) {
249          viewControllerToShow.contentView.transform = CGAffineTransformIdentity;
250          viewControllerToShow.contentView.alpha = 1.0f;
251        }
252      } completion:^(BOOL finished) {
253        transitionFinished();
254      }];
255    } else {
256      transitionFinished();
257    }
258  }
259}
260
261- (void)_animateMenuToVisible:(BOOL)visible completion:(void (^ _Nullable)(void))completion
262{
263  _isAnimatingMenu = YES;
264  __weak typeof(self) weakSelf = self;
265  if (visible) {
266    [_menuViewController willMoveToParentViewController:self];
267
268    if (_menuWindow == nil) {
269      _menuWindow = [[EXMenuWindow alloc] init];
270    }
271
272    [_menuWindow setFrame:self.view.frame];
273    [_menuWindow addSubview:_menuViewController.view];
274    [_menuWindow makeKeyAndVisible];
275
276    _menuViewController.view.alpha = 0.0f;
277    _menuViewController.view.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
278    [UIView animateWithDuration:0.1f animations:^{
279      self.menuViewController.view.alpha = 1.0f;
280      self.menuViewController.view.transform = CGAffineTransformIdentity;
281    } completion:^(BOOL finished) {
282      __strong typeof(weakSelf) strongSelf = weakSelf;
283      if (strongSelf) {
284        strongSelf.isAnimatingMenu = NO;
285        [strongSelf.menuViewController didMoveToParentViewController:self];
286        if (completion) {
287          completion();
288        }
289      }
290    }];
291  } else {
292    _menuViewController.view.alpha = 1.0f;
293    [UIView animateWithDuration:0.1f animations:^{
294      self.menuViewController.view.alpha = 0.0f;
295    } completion:^(BOOL finished) {
296      __strong typeof(weakSelf) strongSelf = weakSelf;
297      if (strongSelf) {
298        strongSelf.isAnimatingMenu = NO;
299        [strongSelf.menuViewController willMoveToParentViewController:nil];
300        [strongSelf.menuViewController.view removeFromSuperview];
301        [strongSelf.menuViewController didMoveToParentViewController:nil];
302        strongSelf.menuWindow = nil;
303        if (completion) {
304          completion();
305        }
306      }
307    }];
308  }
309}
310
311- (EXHomeAppManager *)_getHomeAppManager
312{
313  return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager;
314}
315
316- (void)_maybeResetNuxState
317{
318  // used by appetize: optionally disable nux
319  BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey];
320  if (disableNuxDefaultsValue) {
321    [self setIsNuxFinished:YES];
322    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey];
323  }
324}
325
326- (void)_updateMenuButtonBehavior
327{
328  BOOL shouldShowButton = [[EXKernelDevKeyCommands sharedInstance] isLegacyMenuButtonAvailable];
329  dispatch_async(dispatch_get_main_queue(), ^{
330    self.btnMenu.hidden = !shouldShowButton;
331  });
332}
333
334- (void)_onMenuGestureRecognized:(EXMenuGestureRecognizer *)sender
335{
336  if (sender.state == UIGestureRecognizerStateEnded) {
337    [[EXKernel sharedInstance] switchTasks];
338  }
339}
340
341@end
342
343NS_ASSUME_NONNULL_END
344