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