1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import <React/RCTRootView.h> 4#import <UMCore/UMDefines.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 if (visibleApp.appLoader.manifest && [NSJSONSerialization isValidJSONObject:visibleApp.appLoader.manifest]) { 99 NSError *error; 100 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:visibleApp.appLoader.manifest.rawManifestJSON options:0 error:&error]; 101 if (jsonData) { 102 manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 103 } else { 104 UMLogWarn(@"Failed to serialize JSON manifest: %@", error); 105 } 106 } 107 NSDictionary *task = @{ 108 @"manifestUrl": visibleApp.appLoader.manifestUrl.absoluteString, 109 @"manifestString": manifestString ?: [NSNull null], 110 }; 111 112 return @{ 113 @"task": task, 114 @"uuid": [[NSUUID UUID] UUIDString], // include randomness to force the component to rerender 115 }; 116} 117 118// RCTRootView assumes it is created on a loading bridge. 119// in our case, the bridge has usually already loaded. so we need to prod the view. 120- (void)_forceRootViewToRenderHack 121{ 122 if (!_hasCalledJSLoadedNotification) { 123 RCTBridge *mainBridge = [[EXDevMenuManager sharedInstance] mainBridge]; 124 NSNotification *notif = [[NSNotification alloc] initWithName:RCTJavaScriptDidLoadNotification 125 object:nil 126 userInfo:@{ @"bridge": mainBridge }]; 127 [_reactRootView javaScriptDidLoad:notif]; 128 _hasCalledJSLoadedNotification = YES; 129 } 130} 131 132- (void)_maybeRebuildRootView 133{ 134 RCTBridge *mainBridge = [[EXDevMenuManager sharedInstance] mainBridge]; 135 136 // Main bridge might change if the home bridge restarted for some reason (e.g. due to an error) 137 if (!_reactRootView || _reactRootView.bridge != mainBridge) { 138 if (_reactRootView) { 139 [_reactRootView removeFromSuperview]; 140 _reactRootView = nil; 141 } 142 _hasCalledJSLoadedNotification = NO; 143 144 _reactRootView = [[RCTRootView alloc] initWithBridge:mainBridge moduleName:@"HomeMenu" initialProperties:[self _getInitialPropsForVisibleApp]]; 145 _reactRootView.frame = self.view.bounds; 146 147 // By default react root view has white background, 148 // however devmenu's bottom sheet looks better with partially visible experience. 149 _reactRootView.backgroundColor = [UIColor clearColor]; 150 151 if ([self isViewLoaded]) { 152 [self.view addSubview:_reactRootView]; 153 [self.view setNeedsLayout]; 154 } 155 } else if (_reactRootView) { 156 _reactRootView.appProperties = [self _getInitialPropsForVisibleApp]; 157 } 158} 159 160@end 161