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