1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import <React/RCTRootView.h> 4#import <ExpoModulesCore/EXDefines.h> 5 6#import "EXDevMenuViewController.h" 7#import "EXDevMenuManager.h" 8#import "EXKernel.h" 9#import "EXAbstractLoader.h" 10#import "EXKernelAppRegistry.h" 11#import "EXUtil.h" 12 13@import EXManifests; 14 15@interface EXDevMenuViewController () 16 17@property (nonatomic, strong) RCTRootView *reactRootView; 18@property (nonatomic, assign) BOOL hasCalledJSLoadedNotification; 19 20@end 21 22@interface RCTRootView (EXDevMenuView) 23 24- (void)javaScriptDidLoad:(NSNotification *)notification; 25- (void)hideLoadingView; 26 27@end 28 29@implementation EXDevMenuViewController 30 31# pragma mark - UIViewController 32 33- (void)viewDidLoad 34{ 35 [super viewDidLoad]; 36 37 [self _maybeRebuildRootView]; 38 [self.view addSubview:_reactRootView]; 39} 40 41- (UIRectEdge)edgesForExtendedLayout 42{ 43 return UIRectEdgeNone; 44} 45 46- (BOOL)extendedLayoutIncludesOpaqueBars 47{ 48 return YES; 49} 50 51- (void)viewWillLayoutSubviews 52{ 53 [super viewWillLayoutSubviews]; 54 _reactRootView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 55} 56 57- (void)viewWillAppear:(BOOL)animated 58{ 59 [super viewWillAppear:animated]; 60 [self _maybeRebuildRootView]; 61 [self _forceRootViewToRenderHack]; 62 [_reactRootView becomeFirstResponder]; 63} 64 65- (BOOL)shouldAutorotate 66{ 67 return YES; 68} 69 70/** 71 * Overrides UIViewController's method that returns interface orientations that the view controller supports. 72 * If EXDevMenuViewController is currently shown we want to use its supported orientations so the UI rotates 73 * when we open the dev menu while in the unsupported orientation. 74 * Otherwise, returns interface orientations supported by the current experience. 75 */ 76- (UIInterfaceOrientationMask)supportedInterfaceOrientations 77{ 78 return UIInterfaceOrientationMaskPortrait; 79} 80 81/** 82 * Same case as above with `supportedInterfaceOrientations` method. 83 * If we don't override this, we can get incorrect orientation while changing device orientation when the dev menu is visible. 84 */ 85- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation 86{ 87 return UIInterfaceOrientationPortrait; 88} 89 90#pragma mark - API 91 92 93 94#pragma mark - internal 95 96- (NSDictionary *)_getInitialPropsForVisibleApp 97{ 98 EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp; 99 NSString *manifestString = nil; 100 EXManifestsManifest *manifest = visibleApp.appLoader.manifest; 101 if (manifest && [NSJSONSerialization isValidJSONObject:manifest.rawManifestJSON]) { 102 NSError *error; 103 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:manifest.rawManifestJSON options:0 error:&error]; 104 if (jsonData) { 105 manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 106 } else { 107 EXLogWarn(@"Failed to serialize JSON manifest: %@", error); 108 } 109 } 110 NSDictionary *task = @{ 111 @"manifestUrl": visibleApp.appLoader.manifestUrl.absoluteString, 112 @"manifestString": manifestString ?: [NSNull null], 113 }; 114 115 return @{ 116 @"task": task, 117 @"uuid": [[NSUUID UUID] UUIDString], // include randomness to force the component to rerender 118 }; 119} 120 121// RCTRootView assumes it is created on a loading bridge. 122// in our case, the bridge has usually already loaded. so we need to prod the view. 123- (void)_forceRootViewToRenderHack 124{ 125 if (!_hasCalledJSLoadedNotification) { 126 RCTBridge *mainBridge = [[EXDevMenuManager sharedInstance] mainBridge]; 127 NSNotification *notif = [[NSNotification alloc] initWithName:RCTJavaScriptDidLoadNotification 128 object:nil 129 userInfo:@{ @"bridge": mainBridge }]; 130 [_reactRootView javaScriptDidLoad:notif]; 131 _hasCalledJSLoadedNotification = YES; 132 } 133} 134 135- (void)_maybeRebuildRootView 136{ 137 RCTBridge *mainBridge = [[EXDevMenuManager sharedInstance] mainBridge]; 138 139 // Main bridge might change if the home bridge restarted for some reason (e.g. due to an error) 140 if (!_reactRootView || _reactRootView.bridge != mainBridge) { 141 if (_reactRootView) { 142 [_reactRootView removeFromSuperview]; 143 _reactRootView = nil; 144 } 145 _hasCalledJSLoadedNotification = NO; 146 147 _reactRootView = [[RCTRootView alloc] initWithBridge:mainBridge moduleName:@"HomeMenu" initialProperties:[self _getInitialPropsForVisibleApp]]; 148 149 // By default react root view has white background, 150 // however devmenu's bottom sheet looks better with partially visible experience. 151 _reactRootView.backgroundColor = [UIColor clearColor]; 152 153 if ([self isViewLoaded]) { 154 [self.view addSubview:_reactRootView]; 155 [self.view setNeedsLayout]; 156 } 157 } else if (_reactRootView) { 158 _reactRootView.appProperties = [self _getInitialPropsForVisibleApp]; 159 } 160} 161 162@end 163