xref: /expo/ios/Client/EXRootViewController.m (revision 9155fd2f)
1// Copyright 2015-present 650 Industries. All rights reserved.
2
3@import UIKit;
4
5#import "EXAppDelegate.h"
6#import "EXAppViewController.h"
7#import "EXHomeAppManager.h"
8#import "EXKernel.h"
9#import "EXAppLoader.h"
10#import "EXKernelAppRecord.h"
11#import "EXKernelAppRegistry.h"
12#import "EXKernelLinkingManager.h"
13#import "EXKernelServiceRegistry.h"
14#import "EXRootViewController.h"
15#import "EXDevMenuManager.h"
16
17NSString * const kEXHomeDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey";
18NSString * const kEXHomeIsNuxFinishedDefaultsKey = @"EXHomeIsNuxFinishedDefaultsKey";
19
20NS_ASSUME_NONNULL_BEGIN
21
22@interface EXRootViewController () <EXAppBrowserController>
23
24@property (nonatomic, assign) BOOL isAnimatingAppTransition;
25
26@end
27
28@implementation EXRootViewController
29
30- (instancetype)init
31{
32  if (self = [super init]) {
33    [EXKernel sharedInstance].browserController = self;
34    [self _maybeResetNuxState];
35  }
36  return self;
37}
38
39#pragma mark - EXViewController
40
41- (void)createRootAppAndMakeVisible
42{
43  EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init];
44  EXAppLoader *homeAppLoader = [[EXAppLoader alloc] initWithLocalManifest:[EXHomeAppManager bundledHomeManifest]];
45  EXKernelAppRecord *homeAppRecord = [[EXKernelAppRecord alloc] initWithAppLoader:homeAppLoader appManager:homeAppManager];
46  [[EXKernel sharedInstance].appRegistry registerHomeAppRecord:homeAppRecord];
47  [self moveAppToVisible:homeAppRecord];
48}
49
50#pragma mark - EXAppBrowserController
51
52- (void)moveAppToVisible:(EXKernelAppRecord *)appRecord
53{
54  [self _foregroundAppRecord:appRecord];
55
56  // When foregrounding the app record we want to add it to the history to handle the edge case
57  // where a user opened a project, then went to home and cleared history, then went back to a
58  // the already open project.
59  [self addHistoryItemWithUrl:appRecord.appLoader.manifestUrl manifest:appRecord.appLoader.manifest];
60
61}
62
63- (void)showQRReader
64{
65  [self moveHomeToVisible];
66  [[self _getHomeAppManager] showQRReader];
67}
68
69- (void)moveHomeToVisible
70{
71  [[EXDevMenuManager sharedInstance] close];
72  [self moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord];
73}
74
75// this is different from Util.reload()
76// because it can work even on an errored app record (e.g. with no manifest, or with no running bridge).
77- (void)reloadVisibleApp
78{
79  [[EXDevMenuManager sharedInstance] close];
80
81  EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp;
82  [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp];
83  NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl;
84
85  // Unregister visible app record so all modules get destroyed.
86  [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp];
87
88  // Create new app record.
89  [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil];
90}
91
92- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(NSDictionary *)manifest
93{
94  [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest];
95}
96
97- (void)getHistoryUrlForExperienceId:(NSString *)experienceId completion:(void (^)(NSString *))completion
98{
99  return [[self _getHomeAppManager] getHistoryUrlForExperienceId:experienceId completion:completion];
100}
101
102- (void)setIsNuxFinished:(BOOL)isFinished
103{
104  [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey];
105  [[NSUserDefaults standardUserDefaults] synchronize];
106}
107
108- (BOOL)isNuxFinished
109{
110  return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey];
111}
112
113- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord
114{
115  // show nux if needed
116  if (!self.isNuxFinished
117      && appRecord == [EXKernel sharedInstance].visibleApp
118      && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord) {
119    [[EXDevMenuManager sharedInstance] open];
120  }
121}
122
123#pragma mark - internal
124
125- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord
126{
127  if (_isAnimatingAppTransition) {
128    return;
129  }
130  EXAppViewController *viewControllerToShow = appRecord.viewController;
131  EXAppViewController *viewControllerToHide;
132  if (viewControllerToShow != self.contentViewController) {
133    _isAnimatingAppTransition = YES;
134    if (self.contentViewController) {
135      viewControllerToHide = (EXAppViewController *)self.contentViewController;
136    }
137    if (viewControllerToShow) {
138      [viewControllerToShow willMoveToParentViewController:self];
139      [self.view addSubview:viewControllerToShow.view];
140      [viewControllerToShow foregroundControllers];
141    }
142
143    __weak typeof(self) weakSelf = self;
144    void (^transitionFinished)(void) = ^{
145      __strong typeof(weakSelf) strongSelf = weakSelf;
146      if (strongSelf) {
147        if (viewControllerToHide) {
148          // backgrounds and then dismisses all modals that are presented by the app
149          [viewControllerToHide backgroundControllers];
150          [viewControllerToHide dismissViewControllerAnimated:NO completion:nil];
151          [viewControllerToHide willMoveToParentViewController:nil];
152          [viewControllerToHide.view removeFromSuperview];
153          [viewControllerToHide didMoveToParentViewController:nil];
154        }
155        if (viewControllerToShow) {
156          [viewControllerToShow didMoveToParentViewController:strongSelf];
157          strongSelf.contentViewController = viewControllerToShow;
158        }
159        [strongSelf.view setNeedsLayout];
160        strongSelf.isAnimatingAppTransition = NO;
161        if (strongSelf.delegate) {
162          [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord];
163        }
164      }
165    };
166
167    BOOL animated = (viewControllerToHide && viewControllerToShow);
168    if (animated) {
169      if (viewControllerToHide.contentView) {
170        viewControllerToHide.contentView.transform = CGAffineTransformIdentity;
171        viewControllerToHide.contentView.alpha = 1.0f;
172      }
173      if (viewControllerToShow.contentView) {
174        viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
175        viewControllerToShow.contentView.alpha = 0;
176      }
177      [UIView animateWithDuration:0.3f animations:^{
178        if (viewControllerToHide.contentView) {
179          viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f);
180          viewControllerToHide.contentView.alpha = 0.5f;
181        }
182        if (viewControllerToShow.contentView) {
183          viewControllerToShow.contentView.transform = CGAffineTransformIdentity;
184          viewControllerToShow.contentView.alpha = 1.0f;
185        }
186      } completion:^(BOOL finished) {
187        transitionFinished();
188      }];
189    } else {
190      transitionFinished();
191    }
192  }
193}
194
195- (EXHomeAppManager *)_getHomeAppManager
196{
197  return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager;
198}
199
200- (void)_maybeResetNuxState
201{
202  // used by appetize: optionally disable nux
203  BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey];
204  if (disableNuxDefaultsValue) {
205    [self setIsNuxFinished:YES];
206    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey];
207  }
208}
209
210@end
211
212NS_ASSUME_NONNULL_END
213