Skip to content

Commit

Permalink
[Init] Add an executor provider to RCTBridge's init method
Browse files Browse the repository at this point in the history
If you construct an RCTBridge you may want to configure the executor. However the constructor synchronously calls `setUp` and initializes the executor. Instead, let the code that constructs the bridge specify an executor source, which controls how the executor is provided. This allows for customizing the web view executor with a UIWebView of your choice, or configuring the URL of the debugger proxy that the web socket executor connects to.

Now that RCTRootView takes a bridge in one of its initializers, it is possible to create a bridge with a custom executor and then use that to set up a root view as well.

Test Plan: Run the UIExplorer app and confirm I am able to connect to the plain JSContext via Safari. Hit Cmd-D, pick Chrome and see Chrome open a Tab. Hit Cmd-N and see that I can connect to a plain JSContext again. Shake the simulator and select "Enable Safari Debugging" and see that I can connect to the UIWebView from Safari. tl;dr the keyboard shortcuts and dev menu work as expected.

Fixes #288
  • Loading branch information
ide committed May 31, 2015
1 parent 69ce448 commit 66be23c
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 92 deletions.
17 changes: 13 additions & 4 deletions React/Base/RCTBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

@class RCTBridge;
@class RCTEventDispatcher;
@protocol RCTJavaScriptExecutorSource;

/**
* This notification triggers a reload of all bridges currently running.
Expand All @@ -36,7 +37,7 @@ extern NSString *const RCTJavaScriptDidFailToLoadNotification;
/**
* This block can be used to instantiate modules that require additional
* init parameters, or additional configuration prior to being used.
* The bridge will call this block to instatiate the modules, and will
* The bridge will call this block to instantiate the modules, and will
* be responsible for invalidating/releasing them when the bridge is destroyed.
* For this reason, the block should always return new module instances, and
* module instances should not be shared between bridges.
Expand All @@ -57,13 +58,18 @@ RCT_EXTERN NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass);
* The designated initializer. This creates a new bridge on top of the specified
* executor. The bridge should then be used for all subsequent communication
* with the JavaScript code running in the executor. Modules will be automatically
* instantiated using the default contructor, but you can optionally pass in an
* instantiated using the default constructor, but you can optionally pass in an
* array of pre-initialized module instances if they require additional init
* parameters or configuration.
*/
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleProvider:(RCTBridgeModuleProviderBlock)block
launchOptions:(NSDictionary *)launchOptions NS_DESIGNATED_INITIALIZER;
launchOptions:(NSDictionary *)launchOptions
executorSource:(id<RCTJavaScriptExecutorSource>)executorSource NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleProvider:(RCTBridgeModuleProviderBlock)block
launchOptions:(NSDictionary *)launchOptions;

/**
* This method is used to call functions in the JavaScript application context.
Expand All @@ -90,7 +96,10 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method;
*/
@property (nonatomic, copy) NSURL *bundleURL;

@property (nonatomic, strong) Class executorClass;
/**
* The source of JavaScript executors used by this bridge.
*/
@property (nonatomic, strong, readonly) id<RCTJavaScriptExecutorSource> executorSource;

/**
* The event dispatcher is a wrapper around -enqueueJSCall:args: that provides a
Expand Down
28 changes: 17 additions & 11 deletions React/Base/RCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#import "RCTRootView.h"
#import "RCTSourceCode.h"
#import "RCTSparseArray.h"
#import "RCTStandardExecutorSource.h"
#import "RCTUtils.h"

NSString *const RCTReloadNotification = @"RCTReloadNotification";
Expand Down Expand Up @@ -764,6 +765,17 @@ @implementation RCTBridge
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleProvider:(RCTBridgeModuleProviderBlock)block
launchOptions:(NSDictionary *)launchOptions
{
return [self initWithBundleURL:bundleURL
moduleProvider:block
launchOptions:launchOptions
executorSource:[[RCTStandardExecutorSource alloc] init]];
}

- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleProvider:(RCTBridgeModuleProviderBlock)block
launchOptions:(NSDictionary *)launchOptions
executorSource:(id<RCTJavaScriptExecutorSource>)executorSource
{
RCTAssertMainThread();

Expand All @@ -776,6 +788,7 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL
_bundleURL = bundleURL;
_moduleProvider = block;
_launchOptions = [launchOptions copy];
_executorSource = executorSource;
[self bindKeys];
[self setUp];
}
Expand Down Expand Up @@ -932,8 +945,8 @@ - (instancetype)initWithParentBridge:(RCTBridge *)bridge
/**
* Initialize executor to allow enqueueing calls
*/
Class executorClass = self.executorClass ?: [RCTContextExecutor class];
_javaScriptExecutor = RCTCreateExecutor(executorClass);
_javaScriptExecutor = [_parentBridge.executorSource executor];
RCTSetNewExecutorID(_javaScriptExecutor);
_latestJSExecutor = _javaScriptExecutor;

/**
Expand Down Expand Up @@ -971,16 +984,9 @@ - (void)reload
[_parentBridge reload];
}

- (Class)executorClass
- (id<RCTJavaScriptExecutorSource>)executorSource
{
return _parentBridge.executorClass;
}

- (void)setExecutorClass:(Class)executorClass
{
RCTAssertMainThread();

_parentBridge.executorClass = executorClass;
return _parentBridge.executorSource;
}

- (BOOL)isLoading
Expand Down
125 changes: 57 additions & 68 deletions React/Base/RCTDevMenu.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
#import "RCTLog.h"
#import "RCTPerfStats.h"
#import "RCTProfile.h"
#import "RCTRootView.h"
#import "RCTRedBox.h"
#import "RCTJavaScriptExecutorSource.h"
#import "RCTStandardExecutorSource.h"
#import "RCTSourceCode.h"
#import "RCTUtils.h"

Expand Down Expand Up @@ -45,7 +47,7 @@ - (void)RCT_motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event

@interface RCTDevMenu () <RCTBridgeModule, UIActionSheetDelegate>

@property (nonatomic, strong) Class executorClass;
@property (nonatomic, assign) RCTStandardExecutorType executorType;

@end

Expand Down Expand Up @@ -114,7 +116,7 @@ - (instancetype)init
[commands registerKeyCommandWithInput:@"n"
modifierFlags:UIKeyModifierCommand
action:^(UIKeyCommand *command) {
weakSelf.executorClass = Nil;
weakSelf.executorType = RCTStandardExecutorTypeDefault;
}];
#endif

Expand Down Expand Up @@ -148,7 +150,7 @@ - (void)updateSettings
self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue];
self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue];
self.showFPS = [_settings[@"showFPS"] ?: @NO boolValue];
self.executorClass = NSClassFromString(_settings[@"executorClass"]);
self.executorType = [_settings[@"executorType"] ?: @(RCTStandardExecutorTypeDefault) unsignedIntegerValue];
}

- (void)jsLoaded:(NSNotification *)notification
Expand Down Expand Up @@ -177,7 +179,7 @@ - (void)jsLoaded:(NSNotification *)notification
// Hit these setters again after bridge has finished loading
self.profilingEnabled = _profilingEnabled;
self.liveReloadEnabled = _liveReloadEnabled;
self.executorClass = _executorClass;
self.executorType = _executorType;
});
}

Expand Down Expand Up @@ -231,16 +233,24 @@ - (void)toggle
return;
}

NSString *debugTitleChrome = _executorClass && _executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Debug in Chrome";
NSString *debugTitleSafari = _executorClass && _executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Debug in Safari";
NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor";

UIActionSheet *actionSheet =
[[UIActionSheet alloc] initWithTitle:@"React Native: Development"
delegate:self
cancelButtonTitle:nil
destructiveButtonTitle:nil
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, fpsMonitor, nil];
otherButtonTitles:@"Reload", nil];

if ([_bridge.executorSource isKindOfClass:[RCTStandardExecutorSource class]]) {
RCTStandardExecutorSource *source = (RCTStandardExecutorSource *)_bridge.executorSource;
RCTStandardExecutorType executorType = source.executorType;
NSString *debugTitleChrome = (executorType == RCTStandardExecutorTypeWebSocket) ? @"Disable Chrome Debugging" : @"Debug in Chrome";
NSString *debugTitleSafari = (executorType == RCTStandardExecutorTypeUIWebView) ? @"Disable Safari Debugging" : @"Debug in Safari";
[actionSheet addButtonWithTitle:debugTitleChrome];
[actionSheet addButtonWithTitle:debugTitleSafari];
}

NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor";
[actionSheet addButtonWithTitle:fpsMonitor];

[actionSheet addButtonWithTitle:@"Inspect Element"];

Expand Down Expand Up @@ -275,47 +285,25 @@ - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger
return;
}

switch (buttonIndex) {
case 0: {
[self reload];
break;
}
case 1: {
Class cls = NSClassFromString(@"RCTWebSocketExecutor");
if (!cls) {
[[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable"
message:@"You need to include the RCTWebSocket library to enable Chrome debugging"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
return;
}
self.executorClass = (_executorClass == cls) ? Nil : cls;
break;
}
case 2: {
Class cls = NSClassFromString(@"RCTWebViewExecutor");
self.executorClass = (_executorClass == cls) ? Nil : cls;
break;
}
case 3: {
self.showFPS = !_showFPS;
break;
}
case 4: {
[_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
break;
}
case 5: {
self.liveReloadEnabled = !_liveReloadEnabled;
break;
}
case 6: {
self.profilingEnabled = !_profilingEnabled;
break;
}
default:
break;
if (buttonIndex == 0) {
[self reload];
return;
}

// Note: after supporting iOS 8+, use UIAlertController which has a more cohesive API
NSString *buttonTitle = [actionSheet buttonTitleAtIndex:buttonIndex];
if ([buttonTitle containsString:@"Chrome"]) {
[self _toggleExecutorType:RCTStandardExecutorTypeWebSocket];
} else if ([buttonTitle containsString:@"Safari"]) {
[self _toggleExecutorType:RCTStandardExecutorTypeUIWebView];
} else if ([buttonTitle containsString:@"FPS Monitor"]) {
self.showFPS = !_showFPS;
} else if ([buttonTitle containsString:@"Inspect Element"]) {
[_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
} else if ([buttonTitle containsString:@"Live Reload"]) {
self.liveReloadEnabled = !_liveReloadEnabled;
} else if ([buttonTitle containsString:@"Profiling"]) {
self.profilingEnabled = !_profilingEnabled;
}
}

Expand Down Expand Up @@ -358,27 +346,28 @@ - (void)setLiveReloadEnabled:(BOOL)enabled
}
}

- (void)setExecutorClass:(Class)executorClass
- (void)_toggleExecutorType:(RCTStandardExecutorType)executorType
{
if (_executorClass != executorClass) {
_executorClass = executorClass;
[self updateSetting:@"executorClass" value: NSStringFromClass(executorClass)];
if (_executorType == executorType) {
self.executorType = RCTStandardExecutorTypeDefault;
} else {
self.executorType = executorType;
}
}

if (_bridge.executorClass != executorClass) {

// TODO (6929129): we can remove this special case test once we have better
// support for custom executors in the dev menu. But right now this is
// needed to prevent overriding a custom executor with the default if a
// custom executor has been set directly on the bridge
if (executorClass == Nil &&
(_bridge.executorClass != NSClassFromString(@"RCTWebSocketExecutor") &&
_bridge.executorClass != NSClassFromString(@"RCTWebViewExecutor"))) {
return;
}
- (void)setExecutorType:(RCTStandardExecutorType)executorType
{
if (_executorType != executorType) {
_executorType = executorType;
[self updateSetting:@"executorType" value:@(executorType)];
}

_bridge.executorClass = executorClass;
[self reload];
if ([_bridge.executorSource isKindOfClass:[RCTStandardExecutorSource class]]) {
RCTStandardExecutorSource *source = (RCTStandardExecutorSource *)_bridge.executorSource;
if (source.executorType != executorType) {
source.executorType = executorType;
[_bridge reload];
}
}
}

Expand Down
4 changes: 1 addition & 3 deletions React/Base/RCTJavaScriptExecutor.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,12 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error);
@end

static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID";
__used static id<RCTJavaScriptExecutor> RCTCreateExecutor(Class executorClass)
__used static void RCTSetNewExecutorID(id<RCTJavaScriptExecutor> executor)
{
static NSUInteger executorID = 0;
id<RCTJavaScriptExecutor> executor = [[executorClass alloc] init];
if (executor) {
objc_setAssociatedObject(executor, RCTJavaScriptExecutorID, @(++executorID), OBJC_ASSOCIATION_RETAIN);
}
return executor;
}

__used static NSNumber *RCTGetExecutorID(id<RCTJavaScriptExecutor> executor)
Expand Down
21 changes: 21 additions & 0 deletions React/Base/RCTJavaScriptExecutorSource.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import <Foundation/Foundation.h>

@protocol RCTJavaScriptExecutor;

@protocol RCTJavaScriptExecutorSource <NSObject>

/**
* Return a new JavaScript executor to run a React application.
*/
- (id<RCTJavaScriptExecutor>)executor;

@end
10 changes: 6 additions & 4 deletions React/Base/RCTRootView.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

#import "RCTBridge.h"

@class RCTStandardExecutorSource;

/**
* This notification is sent when the first subviews are added to the root view
* after the application has loaded. This is used to hide the `loadingView`, and
Expand Down Expand Up @@ -63,11 +65,11 @@ extern NSString *const RCTContentDidAppearNotification;
@property (nonatomic, copy) NSDictionary *initialProperties;

/**
* The class of the RCTJavaScriptExecutor to use with this view.
* If not specified, it will default to using RCTContextExecutor.
* Changes will take effect next time the bundle is reloaded.
* The source of the JavaScript executors that this RCTRootView uses. Set the
* executor type through the provider and reload the RCTRootView for a new
* executor.
*/
@property (nonatomic, strong) Class executorClass;
@property (nonatomic, strong, readonly) RCTStandardExecutorSource *executorSource;

/**
* The backing view controller of the root view.
Expand Down
2 changes: 0 additions & 2 deletions React/Base/RCTRootView.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTContextExecutor.h"
#import "RCTEventDispatcher.h"
#import "RCTKeyCommands.h"
#import "RCTLog.h"
Expand All @@ -22,7 +21,6 @@
#import "RCTUIManager.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "RCTWebViewExecutor.h"
#import "UIView+React.h"

NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification";
Expand Down
Loading

0 comments on commit 66be23c

Please sign in to comment.