Implemented thread control for exported methods

This commit is contained in:
Nick Lockwood 2015-04-18 10:43:20 -07:00
parent 2b9aaac2ff
commit ead0f2e020
23 changed files with 384 additions and 403 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -59,11 +59,10 @@
return NO; return NO;
} }
// Make sure this test runs first (underscores sort early) otherwise the // Make sure this test runs first because the other tests will tear out the rootView
// other tests will tear out the rootView - (void)testAAA_RootViewLoadsAndRenders
- (void)test__RootViewLoadsAndRenders
{ {
UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController;
RCTAssert([vc.view isKindOfClass:[RCTRootView class]], @"This test must run first."); RCTAssert([vc.view isKindOfClass:[RCTRootView class]], @"This test must run first.");
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
BOOL foundElement = NO; BOOL foundElement = NO;
@ -72,10 +71,8 @@
while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date];
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date];
redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; redboxError = [[RCTRedBox sharedInstance] currentErrorMessage];
foundElement = [self findSubviewInView:vc.view matching:^(UIView *view) {
foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
if ([view respondsToSelector:@selector(attributedText)]) { if ([view respondsToSelector:@selector(attributedText)]) {
NSString *text = [(id)view attributedText].string; NSString *text = [(id)view attributedText].string;
if ([text isEqualToString:@"<View>"]) { if ([text isEqualToString:@"<View>"]) {
@ -120,6 +117,7 @@
[_runner runTest:_cmd module:@"TabBarExample"]; [_runner runTest:_cmd module:@"TabBarExample"];
} }
// Make sure this test runs last
- (void)testZZZ_NotInRecordMode - (void)testZZZ_NotInRecordMode
{ {
RCTAssert(_runner.recordMode == NO, @"Don't forget to turn record mode back to NO before commit."); RCTAssert(_runner.recordMode == NO, @"Don't forget to turn record mode back to NO before commit.");

View File

@ -34,92 +34,88 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options
failureCallback:(RCTResponseSenderBlock)failureCallback failureCallback:(RCTResponseSenderBlock)failureCallback
successCallback:(RCTResponseSenderBlock)successCallback) successCallback:(RCTResponseSenderBlock)successCallback)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ UIActionSheet *actionSheet = [[UIActionSheet alloc] init];
UIActionSheet *actionSheet = [[UIActionSheet alloc] init];
actionSheet.title = options[@"title"]; actionSheet.title = options[@"title"];
for (NSString *option in options[@"options"]) { for (NSString *option in options[@"options"]) {
[actionSheet addButtonWithTitle:option]; [actionSheet addButtonWithTitle:option];
} }
if (options[@"destructiveButtonIndex"]) { if (options[@"destructiveButtonIndex"]) {
actionSheet.destructiveButtonIndex = [options[@"destructiveButtonIndex"] integerValue]; actionSheet.destructiveButtonIndex = [options[@"destructiveButtonIndex"] integerValue];
} }
if (options[@"cancelButtonIndex"]) { if (options[@"cancelButtonIndex"]) {
actionSheet.cancelButtonIndex = [options[@"cancelButtonIndex"] integerValue]; actionSheet.cancelButtonIndex = [options[@"cancelButtonIndex"] integerValue];
} }
actionSheet.delegate = self; actionSheet.delegate = self;
_callbacks[keyForInstance(actionSheet)] = successCallback; _callbacks[RCTKeyForInstance(actionSheet)] = successCallback;
UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window];
if (appWindow == nil) { if (appWindow == nil) {
RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options);
return; return;
} }
[actionSheet showInView:appWindow]; [actionSheet showInView:appWindow];
});
} }
RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options
failureCallback:(RCTResponseSenderBlock)failureCallback failureCallback:(RCTResponseSenderBlock)failureCallback
successCallback:(RCTResponseSenderBlock)successCallback) successCallback:(RCTResponseSenderBlock)successCallback)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ NSMutableArray *items = [NSMutableArray array];
NSMutableArray *items = [NSMutableArray array]; id message = options[@"message"];
id message = options[@"message"]; id url = options[@"url"];
id url = options[@"url"]; if ([message isKindOfClass:[NSString class]]) {
if ([message isKindOfClass:[NSString class]]) { [items addObject:message];
[items addObject:message]; }
} if ([url isKindOfClass:[NSString class]]) {
if ([url isKindOfClass:[NSString class]]) { [items addObject:[NSURL URLWithString:url]];
[items addObject:[NSURL URLWithString:url]]; }
} if ([items count] == 0) {
if ([items count] == 0) { failureCallback(@[@"No `url` or `message` to share"]);
failureCallback(@[@"No `url` or `message` to share"]); return;
return; }
} UIActivityViewController *share = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil];
UIActivityViewController *share = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; UIViewController *ctrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
UIViewController *ctrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; if ([share respondsToSelector:@selector(setCompletionWithItemsHandler:)]) {
if ([share respondsToSelector:@selector(setCompletionWithItemsHandler:)]) { share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { if (activityError) {
if (activityError) { failureCallback(@[[activityError localizedDescription]]);
failureCallback(@[[activityError localizedDescription]]); } else {
} else { successCallback(@[@(completed), (activityType ?: [NSNull null])]);
successCallback(@[@(completed), (activityType ?: [NSNull null])]); }
} };
}; } else {
} else {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0
if (![UIActivityViewController instancesRespondToSelector:@selector(completionWithItemsHandler)]) { if (![UIActivityViewController instancesRespondToSelector:@selector(completionWithItemsHandler)]) {
// Legacy iOS 7 implementation // Legacy iOS 7 implementation
share.completionHandler = ^(NSString *activityType, BOOL completed) { share.completionHandler = ^(NSString *activityType, BOOL completed) {
successCallback(@[@(completed), (activityType ?: [NSNull null])]); successCallback(@[@(completed), (activityType ?: [NSNull null])]);
}; };
} else } else
#endif #endif
{ {
// iOS 8 version // iOS 8 version
share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
successCallback(@[@(completed), (activityType ?: [NSNull null])]); successCallback(@[@(completed), (activityType ?: [NSNull null])]);
}; };
}
} }
[ctrl presentViewController:share animated:YES completion:nil]; }
}); [ctrl presentViewController:share animated:YES completion:nil];
} }
#pragma mark UIActionSheetDelegate Methods #pragma mark UIActionSheetDelegate Methods
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{ {
NSString *key = keyForInstance(actionSheet); NSString *key = RCTKeyForInstance(actionSheet);
RCTResponseSenderBlock callback = _callbacks[key]; RCTResponseSenderBlock callback = _callbacks[key];
if (callback) { if (callback) {
callback(@[@(buttonIndex)]); callback(@[@(buttonIndex)]);
@ -133,7 +129,7 @@ RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options
#pragma mark Private #pragma mark Private
NS_INLINE NSString *keyForInstance(id instance) static NSString *RCTKeyForInstance(id instance)
{ {
return [NSString stringWithFormat:@"%p", instance]; return [NSString stringWithFormat:@"%p", instance];
} }

View File

@ -68,6 +68,11 @@ RCT_EXPORT_MODULE()
return self; return self;
} }
- (dispatch_queue_t)methodQueue
{
return _bridge.uiManager.methodQueue;
}
- (id (^)(CGFloat))interpolateFrom:(CGFloat[])fromArray to:(CGFloat[])toArray count:(NSUInteger)count typeName:(const char *)typeName - (id (^)(CGFloat))interpolateFrom:(CGFloat[])fromArray to:(CGFloat[])toArray count:(NSUInteger)count typeName:(const char *)typeName
{ {
if (count == 1) { if (count == 1) {

View File

@ -33,7 +33,9 @@ typedef struct {
CLLocationAccuracy accuracy; CLLocationAccuracy accuracy;
} RCTLocationOptions; } RCTLocationOptions;
static RCTLocationOptions RCTLocationOptionsWithJSON(id json) @implementation RCTConvert (RCTLocationOptions)
+ (RCTLocationOptions)RCTLocationOptions:(id)json
{ {
NSDictionary *options = [RCTConvert NSDictionary:json]; NSDictionary *options = [RCTConvert NSDictionary:json];
return (RCTLocationOptions){ return (RCTLocationOptions){
@ -43,6 +45,8 @@ static RCTLocationOptions RCTLocationOptionsWithJSON(id json)
}; };
} }
@end
static NSDictionary *RCTPositionError(RCTPositionErrorCode code, NSString *msg /* nil for default */) static NSDictionary *RCTPositionError(RCTPositionErrorCode code, NSString *msg /* nil for default */)
{ {
if (!msg) { if (!msg) {
@ -121,6 +125,7 @@ RCT_EXPORT_MODULE()
- (void)dealloc - (void)dealloc
{ {
[_locationManager stopUpdatingLocation]; [_locationManager stopUpdatingLocation];
_locationManager.delegate = nil;
} }
#pragma mark - Private API #pragma mark - Private API
@ -153,41 +158,33 @@ RCT_EXPORT_MODULE()
#pragma mark - Public API #pragma mark - Public API
RCT_EXPORT_METHOD(startObserving:(NSDictionary *)optionsJSON) RCT_EXPORT_METHOD(startObserving:(RCTLocationOptions)options)
{ {
[self checkLocationConfig]; [self checkLocationConfig];
dispatch_async(dispatch_get_main_queue(), ^{ // Select best options
_observerOptions = options;
for (RCTLocationRequest *request in _pendingRequests) {
_observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy);
}
// Select best options _locationManager.desiredAccuracy = _observerOptions.accuracy;
_observerOptions = RCTLocationOptionsWithJSON(optionsJSON); [self beginLocationUpdates];
for (RCTLocationRequest *request in _pendingRequests) { _observingLocation = YES;
_observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy);
}
_locationManager.desiredAccuracy = _observerOptions.accuracy;
[self beginLocationUpdates];
_observingLocation = YES;
});
} }
RCT_EXPORT_METHOD(stopObserving) RCT_EXPORT_METHOD(stopObserving)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ // Stop observing
_observingLocation = NO;
// Stop observing // Stop updating if no pending requests
_observingLocation = NO; if (_pendingRequests.count == 0) {
[_locationManager stopUpdatingLocation];
// Stop updating if no pending requests }
if (_pendingRequests.count == 0) {
[_locationManager stopUpdatingLocation];
}
});
} }
RCT_EXPORT_METHOD(getCurrentPosition:(NSDictionary *)optionsJSON RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options
withSuccessCallback:(RCTResponseSenderBlock)successBlock withSuccessCallback:(RCTResponseSenderBlock)successBlock
errorCallback:(RCTResponseSenderBlock)errorBlock) errorCallback:(RCTResponseSenderBlock)errorBlock)
{ {
@ -198,56 +195,49 @@ RCT_EXPORT_METHOD(getCurrentPosition:(NSDictionary *)optionsJSON
return; return;
} }
dispatch_async(dispatch_get_main_queue(), ^{ if (![CLLocationManager locationServicesEnabled]) {
if (errorBlock) {
if (![CLLocationManager locationServicesEnabled]) { errorBlock(@[
if (errorBlock) { RCTPositionError(RCTPositionErrorUnavailable, @"Location services disabled.")
errorBlock(@[ ]);
RCTPositionError(RCTPositionErrorUnavailable, @"Location services disabled.")
]);
return;
}
}
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
if (errorBlock) {
errorBlock(@[
RCTPositionError(RCTPositionErrorDenied, nil)
]);
return;
}
}
// Get options
RCTLocationOptions options = RCTLocationOptionsWithJSON(optionsJSON);
// Check if previous recorded location exists and is good enough
if (_lastLocationEvent &&
CFAbsoluteTimeGetCurrent() - [RCTConvert NSTimeInterval:_lastLocationEvent[@"timestamp"]] < options.maximumAge &&
[_lastLocationEvent[@"coords"][@"accuracy"] doubleValue] >= options.accuracy) {
// Call success block with most recent known location
successBlock(@[_lastLocationEvent]);
return; return;
} }
}
// Create request if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
RCTLocationRequest *request = [[RCTLocationRequest alloc] init]; if (errorBlock) {
request.successBlock = successBlock; errorBlock(@[
request.errorBlock = errorBlock ?: ^(NSArray *args){}; RCTPositionError(RCTPositionErrorDenied, nil)
request.options = options; ]);
request.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:options.timeout return;
target:self }
selector:@selector(timeout:) }
userInfo:request
repeats:NO];
[_pendingRequests addObject:request];
// Configure location manager and begin updating location // Check if previous recorded location exists and is good enough
_locationManager.desiredAccuracy = MIN(_locationManager.desiredAccuracy, options.accuracy); if (_lastLocationEvent &&
[self beginLocationUpdates]; CFAbsoluteTimeGetCurrent() - [RCTConvert NSTimeInterval:_lastLocationEvent[@"timestamp"]] < options.maximumAge &&
[_lastLocationEvent[@"coords"][@"accuracy"] doubleValue] >= options.accuracy) {
}); // Call success block with most recent known location
successBlock(@[_lastLocationEvent]);
return;
}
// Create request
RCTLocationRequest *request = [[RCTLocationRequest alloc] init];
request.successBlock = successBlock;
request.errorBlock = errorBlock ?: ^(NSArray *args){};
request.options = options;
request.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:options.timeout
target:self
selector:@selector(timeout:)
userInfo:request
repeats:NO];
[_pendingRequests addObject:request];
// Configure location manager and begin updating location
_locationManager.desiredAccuracy = MIN(_locationManager.desiredAccuracy, options.accuracy);
[self beginLocationUpdates];
} }
#pragma mark - CLLocationManagerDelegate #pragma mark - CLLocationManagerDelegate

View File

@ -62,8 +62,10 @@ RCT_EXPORT_METHOD(openURL:(NSURL *)URL)
RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL
callback:(RCTResponseSenderBlock)callback) callback:(RCTResponseSenderBlock)callback)
{ {
BOOL canOpen = [[UIApplication sharedApplication] canOpenURL:URL]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
callback(@[@(canOpen)]); BOOL canOpen = [[UIApplication sharedApplication] canOpenURL:URL];
callback(@[@(canOpen)]);
});
} }
- (NSDictionary *)constantsToExport - (NSDictionary *)constantsToExport

View File

@ -15,14 +15,24 @@
@interface RCTTestModule : NSObject <RCTBridgeModule> @interface RCTTestModule : NSObject <RCTBridgeModule>
// This is typically polled while running the runloop until true /**
@property (nonatomic, readonly, getter=isDone) BOOL done; * The snapshot test controller for this module.
*/
// This is used to give meaningful names to snapshot image files. @property (nonatomic, weak) FBSnapshotTestController *controller;
@property (nonatomic, assign) SEL testSelector;
/**
* This is the view to be snapshotted.
*/
@property (nonatomic, weak) UIView *view; @property (nonatomic, weak) UIView *view;
- (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view; /**
* This is used to give meaningful names to snapshot image files.
*/
@property (nonatomic, assign) SEL testSelector;
/**
* This is typically polled while running the runloop until true.
*/
@property (nonatomic, readonly, getter=isDone) BOOL done;
@end @end

View File

@ -12,21 +12,25 @@
#import "FBSnapshotTestController.h" #import "FBSnapshotTestController.h"
#import "RCTAssert.h" #import "RCTAssert.h"
#import "RCTLog.h" #import "RCTLog.h"
#import "RCTUIManager.h"
@implementation RCTTestModule @implementation RCTTestModule
{ {
__weak FBSnapshotTestController *_snapshotController;
__weak UIView *_view;
NSMutableDictionary *_snapshotCounter; NSMutableDictionary *_snapshotCounter;
} }
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE() RCT_EXPORT_MODULE()
- (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view - (dispatch_queue_t)methodQueue
{
return _bridge.uiManager.methodQueue;
}
- (instancetype)init
{ {
if ((self = [super init])) { if ((self = [super init])) {
_snapshotController = controller;
_view = view;
_snapshotCounter = [NSMutableDictionary new]; _snapshotCounter = [NSMutableDictionary new];
} }
return self; return self;
@ -34,30 +38,29 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(verifySnapshot:(RCTResponseSenderBlock)callback) RCT_EXPORT_METHOD(verifySnapshot:(RCTResponseSenderBlock)callback)
{ {
if (!_snapshotController) { RCTAssert(_controller != nil, @"No snapshot controller configured.");
RCTLogWarn(@"No snapshot controller configured.");
callback(@[]); [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
NSString *testName = NSStringFromSelector(_testSelector); NSString *testName = NSStringFromSelector(_testSelector);
_snapshotCounter[testName] = @([_snapshotCounter[testName] integerValue] + 1); _snapshotCounter[testName] = [@([_snapshotCounter[testName] integerValue] + 1) stringValue];
NSError *error = nil; NSError *error = nil;
BOOL success = [_snapshotController compareSnapshotOfView:_view BOOL success = [_controller compareSnapshotOfView:_view
selector:_testSelector selector:_testSelector
identifier:[_snapshotCounter[testName] stringValue] identifier:_snapshotCounter[testName]
error:&error]; error:&error];
RCTAssert(success, @"Snapshot comparison failed: %@", error); RCTAssert(success, @"Snapshot comparison failed: %@", error);
callback(@[]); callback(@[]);
}); }];
} }
RCT_EXPORT_METHOD(markTestCompleted) RCT_EXPORT_METHOD(markTestCompleted)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
_done = YES; _done = YES;
}); }];
} }
@end @end

View File

@ -19,7 +19,7 @@
@implementation RCTTestRunner @implementation RCTTestRunner
{ {
FBSnapshotTestController *_snapshotController; FBSnapshotTestController *_testController;
} }
- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir - (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir
@ -27,8 +27,8 @@
if ((self = [super init])) { if ((self = [super init])) {
NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"]; NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"]; sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"];
_snapshotController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName]; _testController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName];
_snapshotController.referenceImagesDirectory = referenceDir; _testController.referenceImagesDirectory = referenceDir;
_scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]]; _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]];
} }
return self; return self;
@ -36,12 +36,12 @@
- (void)setRecordMode:(BOOL)recordMode - (void)setRecordMode:(BOOL)recordMode
{ {
_snapshotController.recordMode = recordMode; _testController.recordMode = recordMode;
} }
- (BOOL)recordMode - (BOOL)recordMode
{ {
return _snapshotController.recordMode; return _testController.recordMode;
} }
- (void)runTest:(SEL)test module:(NSString *)moduleName - (void)runTest:(SEL)test module:(NSString *)moduleName
@ -59,27 +59,22 @@
- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
{ {
UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:_scriptURL
if ([vc.view isKindOfClass:[RCTRootView class]]) { moduleName:moduleName
[(RCTRootView *)vc.view invalidate]; // Make sure the normal app view doesn't interfere launchOptions:nil];
}
vc.view = [[UIView alloc] init];
RCTTestModule *testModule = [[RCTTestModule alloc] initWithSnapshotController:_snapshotController view:nil];
testModule.testSelector = test;
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL
moduleProvider:^(){
return @[testModule];
}
launchOptions:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:moduleName];
testModule.view = rootView;
[vc.view addSubview:rootView]; // Add as subview so it doesn't get resized
rootView.initialProperties = initialProps; rootView.initialProperties = initialProps;
rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices
NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]);
RCTTestModule *testModule = rootView.bridge.modules[testModuleName];
testModule.controller = _testController;
testModule.testSelector = test;
testModule.view = rootView;
UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController;
vc.view = [[UIView alloc] init];
[vc.view addSubview:rootView]; // Add as subview so it doesn't get resized
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage]; NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage];
while ([date timeIntervalSinceNow] > 0 && ![testModule isDone] && error == nil) { while ([date timeIntervalSinceNow] > 0 && ![testModule isDone] && error == nil) {

View File

@ -101,13 +101,6 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method;
*/ */
@property (nonatomic, copy, readonly) NSDictionary *modules; @property (nonatomic, copy, readonly) NSDictionary *modules;
/**
* The shadow queue is used to execute callbacks from the JavaScript code. All
* native hooks (e.g. exported module methods) will be executed on the shadow
* queue.
*/
@property (nonatomic, readonly) dispatch_queue_t shadowQueue;
/** /**
* The launch options that were used to initialize the bridge. * The launch options that were used to initialize the bridge.
*/ */

View File

@ -792,6 +792,7 @@ static NSDictionary *RCTLocalModulesConfig()
@implementation RCTBridge @implementation RCTBridge
{ {
RCTSparseArray *_modulesByID; RCTSparseArray *_modulesByID;
RCTSparseArray *_queuesByID;
NSDictionary *_modulesByName; NSDictionary *_modulesByName;
id<RCTJavaScriptExecutor> _javaScriptExecutor; id<RCTJavaScriptExecutor> _javaScriptExecutor;
Class _executorClass; Class _executorClass;
@ -824,13 +825,13 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
return self; return self;
} }
- (void)setUp - (void)setUp
{ {
Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class]; Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class];
_javaScriptExecutor = [[executorClass alloc] init]; _javaScriptExecutor = [[executorClass alloc] init];
_latestJSExecutor = _javaScriptExecutor; _latestJSExecutor = _javaScriptExecutor;
_eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self];
_shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL);
_displayLink = [[RCTDisplayLink alloc] initWithBridge:self]; _displayLink = [[RCTDisplayLink alloc] initWithBridge:self];
_frameUpdateObservers = [[NSMutableSet alloc] init]; _frameUpdateObservers = [[NSMutableSet alloc] init];
_scheduledCalls = [[NSMutableArray alloc] init]; _scheduledCalls = [[NSMutableArray alloc] init];
@ -848,21 +849,29 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
[RCTBridgeModuleClassesByModuleID() enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) { [RCTBridgeModuleClassesByModuleID() enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
NSString *moduleName = RCTModuleNamesByID[moduleID]; NSString *moduleName = RCTModuleNamesByID[moduleID];
// Check if module instance has already been registered for this name // Check if module instance has already been registered for this name
if ((_modulesByID[moduleID] = modulesByName[moduleName])) { id<RCTBridgeModule> module = modulesByName[moduleName];
if (module) {
// Preregistered instances takes precedence, no questions asked // Preregistered instances takes precedence, no questions asked
if (!preregisteredModules[moduleName]) { if (!preregisteredModules[moduleName]) {
// It's OK to have a name collision as long as the second instance is nil // It's OK to have a name collision as long as the second instance is nil
RCTAssert([[moduleClass alloc] init] == nil, RCTAssert([[moduleClass alloc] init] == nil,
@"Attempted to register RCTBridgeModule class %@ for the name '%@', \ @"Attempted to register RCTBridgeModule class %@ for the name "
but name was already registered by class %@", moduleClass, "'%@', but name was already registered by class %@", moduleClass,
moduleName, [modulesByName[moduleName] class]); moduleName, [modulesByName[moduleName] class]);
} }
if ([module class] != moduleClass) {
RCTLogInfo(@"RCTBridgeModule of class %@ with name '%@' was encountered "
"in the project, but name was already registered by class %@."
"That's fine if it's intentional - just letting you know.",
moduleClass, moduleName, [modulesByName[moduleName] class]);
}
} else { } else {
// Module name hasn't been used before, so go ahead and instantiate // Module name hasn't been used before, so go ahead and instantiate
id<RCTBridgeModule> module = [[moduleClass alloc] init]; module = [[moduleClass alloc] init];
if (module) { }
_modulesByID[moduleID] = modulesByName[moduleName] = module; if (module) {
} // Store module instance
_modulesByID[moduleID] = modulesByName[moduleName] = module;
} }
}]; }];
@ -876,6 +885,14 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
} }
} }
// Get method queue
_queuesByID = [[RCTSparseArray alloc] init];
[_modulesByID enumerateObjectsUsingBlock:^(id<RCTBridgeModule> module, NSNumber *moduleID, BOOL *stop) {
if ([module respondsToSelector:@selector(methodQueue)]) {
_queuesByID[moduleID] = [module methodQueue] ?: dispatch_get_main_queue();
}
}];
// Inject module data into JS context // Inject module data into JS context
NSString *configJSON = RCTJSONStringify(@{ NSString *configJSON = RCTJSONStringify(@{
@"remoteModuleConfig": RCTRemoteModulesConfig(_modulesByName), @"remoteModuleConfig": RCTRemoteModulesConfig(_modulesByName),
@ -1027,6 +1044,7 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
// Release modules (breaks retain cycle if module has strong bridge reference) // Release modules (breaks retain cycle if module has strong bridge reference)
_modulesByID = nil; _modulesByID = nil;
_queuesByID = nil;
_modulesByName = nil; _modulesByName = nil;
} }
@ -1203,6 +1221,8 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
return; return;
} }
// TODO: if we sort the requests by module, we could dispatch once per
// module instead of per request, which would reduce the call overhead.
for (NSUInteger i = 0; i < numRequests; i++) { for (NSUInteger i = 0; i < numRequests; i++) {
@autoreleasepool { @autoreleasepool {
[self _handleRequestNumber:i [self _handleRequestNumber:i
@ -1212,14 +1232,15 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
} }
} }
// TODO: only used by RCTUIManager - can we eliminate this special case? // TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case?
dispatch_async(self.shadowQueue, ^{ [_modulesByID enumerateObjectsUsingBlock:^(id<RCTBridgeModule> module, NSNumber *moduleID, BOOL *stop) {
for (id module in _modulesByID.allObjects) { if ([module respondsToSelector:@selector(batchDidComplete)]) {
if ([module respondsToSelector:@selector(batchDidComplete)]) { dispatch_queue_t queue = _queuesByID[moduleID];
dispatch_async(queue ?: dispatch_get_main_queue(), ^{
[module batchDidComplete]; [module batchDidComplete];
} });
} }
}); }];
} }
- (BOOL)_handleRequestNumber:(NSUInteger)i - (BOOL)_handleRequestNumber:(NSUInteger)i
@ -1241,7 +1262,8 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
RCTModuleMethod *method = methods[methodID]; RCTModuleMethod *method = methods[methodID];
__weak RCTBridge *weakSelf = self; __weak RCTBridge *weakSelf = self;
dispatch_async(self.shadowQueue, ^{ dispatch_queue_t queue = _queuesByID[moduleID];
dispatch_async(queue ?: dispatch_get_main_queue(), ^{
__strong RCTBridge *strongSelf = weakSelf; __strong RCTBridge *strongSelf = weakSelf;
if (!strongSelf.isValid) { if (!strongSelf.isValid) {

View File

@ -89,6 +89,23 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response);
__attribute__((__aligned__(1))) \ __attribute__((__aligned__(1))) \
static const char *__rct_export_entry__[] = { __func__, #js_name, NULL } static const char *__rct_export_entry__[] = { __func__, #js_name, NULL }
/**
* The queue that will be used to call all exported methods. If omitted, this
* will default the main queue, which is recommended for any methods that
* interact with UIKit. If your methods perform heavy work such as filesystem
* or network access, you should return a custom serial queue. Example:
*
* - (dispatch_queue_t)methodQueue
* {
* return dispatch_queue_create("com.mydomain.FileQueue", DISPATCH_QUEUE_SERIAL);
* }
*
* Alternatively, if only some methods on the module should be executed on a
* background queue you can leave this method unimplemented, and simply
* dispatch_async() within the method itself.
*/
- (dispatch_queue_t)methodQueue;
/** /**
* Injects constants into JS. These constants are made accessible via * Injects constants into JS. These constants are made accessible via
* NativeModules.ModuleName.X. This method is called when the module is * NativeModules.ModuleName.X. This method is called when the module is

View File

@ -138,7 +138,7 @@ BOOL RCTCopyProperty(id target, id source, NSString *keyPath);
/** /**
* Underlying implementation of RCT_ENUM_CONVERTER macro. Ignore this. * Underlying implementation of RCT_ENUM_CONVERTER macro. Ignore this.
*/ */
NSNumber *RCTConverterEnumValue(const char *, NSDictionary *, NSNumber *, id); NSNumber *RCTConvertEnumValue(const char *, NSDictionary *, NSNumber *, id);
#ifdef __cplusplus #ifdef __cplusplus
} }
@ -190,7 +190,7 @@ RCT_CUSTOM_CONVERTER(type, type, [[self NSNumber:json] getter])
dispatch_once(&onceToken, ^{ \ dispatch_once(&onceToken, ^{ \
mapping = values; \ mapping = values; \
}); \ }); \
NSNumber *converted = RCTConverterEnumValue(#type, mapping, @(default), json); \ NSNumber *converted = RCTConvertEnumValue(#type, mapping, @(default), json); \
return ((type(*)(id, SEL))objc_msgSend)(converted, @selector(getter)); \ return ((type(*)(id, SEL))objc_msgSend)(converted, @selector(getter)); \
} }

View File

@ -58,6 +58,10 @@ RCT_CONVERTER(NSString *, NSString, description)
+ (NSURL *)NSURL:(id)json + (NSURL *)NSURL:(id)json
{ {
if (!json || json == (id)kCFNull) {
return nil;
}
if (![json isKindOfClass:[NSString class]]) { if (![json isKindOfClass:[NSString class]]) {
RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json); RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json);
return nil; return nil;
@ -115,7 +119,7 @@ RCT_CUSTOM_CONVERTER(NSTimeInterval, NSTimeInterval, [self double:json] / 1000.0
// JS standard for time zones is minutes. // JS standard for time zones is minutes.
RCT_CUSTOM_CONVERTER(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[self double:json] * 60.0]) RCT_CUSTOM_CONVERTER(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[self double:json] * 60.0])
NSNumber *RCTConverterEnumValue(const char *typeName, NSDictionary *mapping, NSNumber *defaultValue, id json) NSNumber *RCTConvertEnumValue(const char *typeName, NSDictionary *mapping, NSNumber *defaultValue, id json)
{ {
if (!json || json == (id)kCFNull) { if (!json || json == (id)kCFNull) {
return defaultValue; return defaultValue;

View File

@ -64,37 +64,34 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
return; return;
} }
dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title
message:message
delegate:self
cancelButtonTitle:nil
otherButtonTitles:nil];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count];
message:message
delegate:self
cancelButtonTitle:nil
otherButtonTitles:nil];
NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count]; NSInteger index = 0;
for (NSDictionary *button in buttons) {
NSInteger index = 0; if (button.count != 1) {
for (NSDictionary *button in buttons) { RCTLogError(@"Button definitions should have exactly one key.");
if (button.count != 1) {
RCTLogError(@"Button definitions should have exactly one key.");
}
NSString *buttonKey = [button.allKeys firstObject];
NSString *buttonTitle = [button[buttonKey] description];
[alertView addButtonWithTitle:buttonTitle];
if ([buttonKey isEqualToString: @"cancel"]) {
alertView.cancelButtonIndex = index;
}
[buttonKeys addObject:buttonKey];
index ++;
} }
NSString *buttonKey = [button.allKeys firstObject];
NSString *buttonTitle = [button[buttonKey] description];
[alertView addButtonWithTitle:buttonTitle];
if ([buttonKey isEqualToString: @"cancel"]) {
alertView.cancelButtonIndex = index;
}
[buttonKeys addObject:buttonKey];
index ++;
}
[_alerts addObject:alertView]; [_alerts addObject:alertView];
[_alertCallbacks addObject:callback ?: ^(id unused) {}]; [_alertCallbacks addObject:callback ?: ^(id unused) {}];
[_alertButtonKeys addObject:buttonKeys]; [_alertButtonKeys addObject:buttonKeys];
[alertView show]; [alertView show];
});
} }
#pragma mark - UIAlertViewDelegate #pragma mark - UIAlertViewDelegate

View File

@ -61,20 +61,6 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut
return nil; return nil;
} }
static dispatch_queue_t RCTFileQueue(void)
{
static dispatch_queue_t fileQueue = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// All JS is single threaded, so a serial queue is our only option.
fileQueue = dispatch_queue_create("com.facebook.rkFile", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(fileQueue,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
});
return fileQueue;
}
#pragma mark - RCTAsyncLocalStorage #pragma mark - RCTAsyncLocalStorage
@implementation RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage
@ -90,6 +76,11 @@ static dispatch_queue_t RCTFileQueue(void)
RCT_EXPORT_MODULE() RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}
- (NSString *)_filePathForKey:(NSString *)key - (NSString *)_filePathForKey:(NSString *)key
{ {
NSString *safeFileName = RCTMD5Hash(key); NSString *safeFileName = RCTMD5Hash(key);
@ -196,99 +187,89 @@ RCT_EXPORT_METHOD(multiGet:(NSArray *)keys
return; return;
} }
dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup];
id errorOut = [self _ensureSetup]; if (errorOut) {
if (errorOut) { callback(@[@[errorOut], [NSNull null]]);
callback(@[@[errorOut], [NSNull null]]); return;
return; }
} NSMutableArray *errors;
NSMutableArray *errors; NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count];
NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; for (NSString *key in keys) {
for (NSString *key in keys) { id keyError = [self _appendItemForKey:key toArray:result];
id keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors);
RCTAppendError(keyError, &errors); }
} [self _writeManifest:&errors];
[self _writeManifest:&errors]; callback(@[errors ?: [NSNull null], result]);
callback(@[errors ?: [NSNull null], result]);
});
} }
RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs
callback:(RCTResponseSenderBlock)callback) callback:(RCTResponseSenderBlock)callback)
{ {
dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup];
id errorOut = [self _ensureSetup]; if (errorOut) {
if (errorOut) { callback(@[@[errorOut]]);
callback(@[@[errorOut]]); return;
return; }
} NSMutableArray *errors;
NSMutableArray *errors; for (NSArray *entry in kvPairs) {
for (NSArray *entry in kvPairs) { id keyError = [self _writeEntry:entry];
id keyError = [self _writeEntry:entry]; RCTAppendError(keyError, &errors);
RCTAppendError(keyError, &errors); }
} [self _writeManifest:&errors];
[self _writeManifest:&errors]; if (callback) {
if (callback) { callback(@[errors ?: [NSNull null]]);
callback(@[errors ?: [NSNull null]]); }
}
});
} }
RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys
callback:(RCTResponseSenderBlock)callback) callback:(RCTResponseSenderBlock)callback)
{ {
dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup];
id errorOut = [self _ensureSetup]; if (errorOut) {
if (errorOut) { callback(@[@[errorOut]]);
callback(@[@[errorOut]]); return;
return; }
NSMutableArray *errors;
for (NSString *key in keys) {
id keyError = RCTErrorForKey(key);
if (!keyError) {
NSString *filePath = [self _filePathForKey:key];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
[_manifest removeObjectForKey:key];
} }
NSMutableArray *errors; RCTAppendError(keyError, &errors);
for (NSString *key in keys) { }
id keyError = RCTErrorForKey(key); [self _writeManifest:&errors];
if (!keyError) { if (callback) {
NSString *filePath = [self _filePathForKey:key]; callback(@[errors ?: [NSNull null]]);
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; }
[_manifest removeObjectForKey:key];
}
RCTAppendError(keyError, &errors);
}
[self _writeManifest:&errors];
if (callback) {
callback(@[errors ?: [NSNull null]]);
}
});
} }
RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback)
{ {
dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup];
id errorOut = [self _ensureSetup]; if (!errorOut) {
if (!errorOut) { NSError *error;
NSError *error; for (NSString *key in _manifest) {
for (NSString *key in _manifest) { NSString *filePath = [self _filePathForKey:key];
NSString *filePath = [self _filePathForKey:key]; [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
}
[_manifest removeAllObjects];
errorOut = [self _writeManifest:nil];
} }
if (callback) { [_manifest removeAllObjects];
callback(@[errorOut ?: [NSNull null]]); errorOut = [self _writeManifest:nil];
} }
}); if (callback) {
callback(@[errorOut ?: [NSNull null]]);
}
} }
RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback)
{ {
dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup];
id errorOut = [self _ensureSetup]; if (errorOut) {
if (errorOut) { callback(@[errorOut, [NSNull null]]);
callback(@[errorOut, [NSNull null]]); } else {
} else { callback(@[[NSNull null], [_manifest allKeys]]);
callback(@[[NSNull null], [_manifest allKeys]]); }
}
});
} }
@end @end

View File

@ -48,13 +48,11 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message
} }
#ifdef DEBUG #ifdef DEBUG
[[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack];
#else #else
if (RCTReloadRetries < _maxReloadAttempts) { if (RCTReloadRetries < _maxReloadAttempts) {
RCTReloadRetries++; RCTReloadRetries++;
dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil];
});
} else { } else {
NSError *error; NSError *error;
const NSUInteger MAX_SANITIZED_LENGTH = 75; const NSUInteger MAX_SANITIZED_LENGTH = 75;

View File

@ -24,7 +24,6 @@ RCT_EXPORT_METHOD(getScriptText:(RCTResponseSenderBlock)successCallback
} else { } else {
failureCallback(@[RCTMakeError(@"Source code is not available", nil, nil)]); failureCallback(@[RCTMakeError(@"Source code is not available", nil, nil)]);
} }
} }
@end @end

View File

@ -29,31 +29,25 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(setStyle:(UIStatusBarStyle)statusBarStyle RCT_EXPORT_METHOD(setStyle:(UIStatusBarStyle)statusBarStyle
animated:(BOOL)animated) animated:(BOOL)animated)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ if (RCTViewControllerBasedStatusBarAppearance()) {
RCTLogError(@"RCTStatusBarManager module requires that the \
if (RCTViewControllerBasedStatusBarAppearance()) { UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO");
RCTLogError(@"RCTStatusBarManager module requires that the \ } else {
UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle
} else { animated:animated];
[[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle }
animated:animated];
}
});
} }
RCT_EXPORT_METHOD(setHidden:(BOOL)hidden RCT_EXPORT_METHOD(setHidden:(BOOL)hidden
withAnimation:(UIStatusBarAnimation)animation) withAnimation:(UIStatusBarAnimation)animation)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ if (RCTViewControllerBasedStatusBarAppearance()) {
RCTLogError(@"RCTStatusBarManager module requires that the \
if (RCTViewControllerBasedStatusBarAppearance()) { UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO");
RCTLogError(@"RCTStatusBarManager module requires that the \ } else {
UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); [[UIApplication sharedApplication] setStatusBarHidden:hidden
} else { withAnimation:animation];
[[UIApplication sharedApplication] setStatusBarHidden:hidden }
withAnimation:animation];
}
});
} }
- (NSDictionary *)constantsToExport - (NSDictionary *)constantsToExport

View File

@ -187,21 +187,17 @@ RCT_EXPORT_METHOD(createTimer:(NSNumber *)callbackID
interval:jsDuration interval:jsDuration
targetTime:targetTime targetTime:targetTime
repeats:repeats]; repeats:repeats];
dispatch_async(dispatch_get_main_queue(), ^{ _timers[callbackID] = timer;
_timers[callbackID] = timer; [self startTimers];
[self startTimers];
});
} }
RCT_EXPORT_METHOD(deleteTimer:(NSNumber *)timerID) RCT_EXPORT_METHOD(deleteTimer:(NSNumber *)timerID)
{ {
if (timerID) { if (timerID) {
dispatch_async(dispatch_get_main_queue(), ^{ _timers[timerID] = nil;
_timers[timerID] = nil; if (_timers.count == 0) {
if (_timers.count == 0) { [self stopTimers];
[self stopTimers]; }
}
});
} else { } else {
RCTLogWarn(@"Called deleteTimer: with a nil timerID"); RCTLogWarn(@"Called deleteTimer: with a nil timerID");
} }

View File

@ -178,7 +178,7 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio
@implementation RCTUIManager @implementation RCTUIManager
{ {
__weak dispatch_queue_t _shadowQueue; dispatch_queue_t _shadowQueue;
// Root views are only mutated on the shadow queue // Root views are only mutated on the shadow queue
NSMutableSet *_rootViewTags; NSMutableSet *_rootViewTags;
@ -242,24 +242,12 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa
}; };
} }
/**
* This private constructor should only be called when creating
* isolated UIImanager instances for testing. Normal initialization
* is via -init:, which is called automatically by the bridge.
*/
- (instancetype)initWithShadowQueue:(dispatch_queue_t)shadowQueue
{
if ((self = [self init])) {
_shadowQueue = shadowQueue;
_viewManagers = [[NSMutableDictionary alloc] init];
}
return self;
}
- (instancetype)init - (instancetype)init
{ {
if ((self = [super init])) { if ((self = [super init])) {
_shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL);
_pendingUIBlocksLock = [[NSLock alloc] init]; _pendingUIBlocksLock = [[NSLock alloc] init];
_defaultShadowViews = [[NSMutableDictionary alloc] init]; _defaultShadowViews = [[NSMutableDictionary alloc] init];
@ -310,7 +298,6 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa
RCTAssert(_bridge == nil, @"Should not re-use same UIIManager instance"); RCTAssert(_bridge == nil, @"Should not re-use same UIIManager instance");
_bridge = bridge; _bridge = bridge;
_shadowQueue = _bridge.shadowQueue;
_shadowViewRegistry = [[RCTSparseArray alloc] init]; _shadowViewRegistry = [[RCTSparseArray alloc] init];
// Get view managers from bridge // Get view managers from bridge
@ -328,6 +315,11 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa
_viewConfigs = [viewConfigs copy]; _viewConfigs = [viewConfigs copy];
} }
- (dispatch_queue_t)methodQueue
{
return _shadowQueue;
}
- (void)registerRootView:(UIView *)rootView; - (void)registerRootView:(UIView *)rootView;
{ {
RCTAssertMainThread(); RCTAssertMainThread();
@ -366,7 +358,7 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa
NSNumber *reactTag = rootView.reactTag; NSNumber *reactTag = rootView.reactTag;
RCTAssert(RCTIsReactRootView(reactTag), @"Specified view %@ is not a root view", reactTag); RCTAssert(RCTIsReactRootView(reactTag), @"Specified view %@ is not a root view", reactTag);
dispatch_async(_bridge.shadowQueue, ^{ dispatch_async(_shadowQueue, ^{
RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag];
RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag);
rootShadowView.frame = frame; rootShadowView.frame = frame;
@ -396,8 +388,6 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa
- (void)addUIBlock:(RCTViewManagerUIBlock)block - (void)addUIBlock:(RCTViewManagerUIBlock)block
{ {
RCTAssert(![NSThread isMainThread], @"This method should only be called on the shadow thread");
if (!self.isValid) { if (!self.isValid) {
return; return;
} }
@ -417,7 +407,7 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa
- (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)rootShadowView - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)rootShadowView
{ {
RCTAssert(![NSThread isMainThread], @"This should never be executed on main thread."); RCTAssert(![NSThread isMainThread], @"Should be called on shadow thread");
NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1]; NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1];
@ -679,6 +669,8 @@ RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry]; [self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
// TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient
// Figure out what to insert - merge temporary inserts and adds // Figure out what to insert - merge temporary inserts and adds
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary]; NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary];
for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) { for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) {
@ -886,8 +878,6 @@ RCT_EXPORT_METHOD(blur:(NSNumber *)reactTag)
- (void)flushUIBlocks - (void)flushUIBlocks
{ {
RCTAssert(![NSThread isMainThread], @"Should be called on shadow thread");
// First copy the previous blocks into a temporary variable, then reset the // First copy the previous blocks into a temporary variable, then reset the
// pending blocks to a new array. This guards against mutation while // pending blocks to a new array. This guards against mutation while
// processing the pending blocks in another thread. // processing the pending blocks in another thread.

View File

@ -109,8 +109,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v
* within the view or shadowView. * within the view or shadowView.
*/ */
#define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type) \ #define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type) \
RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ RCT_CUSTOM_VIEW_PROPERTY(name, type, UIView) { \
- (void)set_##name:(id)json forView:(id)view withDefaultView:(id)defaultView { \
if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \ if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \
(!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \ (!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \
RCTLogError(@"%@ does not have setter for `%s` property", [view class], #name); \ RCTLogError(@"%@ does not have setter for `%s` property", [view class], #name); \
@ -118,8 +117,7 @@ RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \
} }
#define RCT_REMAP_SHADOW_PROPERTY(name, keyPath, type) \ #define RCT_REMAP_SHADOW_PROPERTY(name, keyPath, type) \
RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ RCT_CUSTOM_SHADOW_PROPERTY(name, type, RCTShadowView) { \
- (void)set_##name:(id)json forShadowView:(id)view withDefaultView:(id)defaultView { \
if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \ if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \
(!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \ (!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \
RCTLogError(@"%@ does not have setter for `%s` property", [view class], #name); \ RCTLogError(@"%@ does not have setter for `%s` property", [view class], #name); \
@ -132,11 +130,11 @@ RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \
* refer to "json", "view" and "defaultView" to implement the required logic. * refer to "json", "view" and "defaultView" to implement the required logic.
*/ */
#define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass) \ #define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass) \
RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ + (NSDictionary *)getPropConfigView_##name { return @{@"name": @#name, @"type": @#type}; } \
- (void)set_##name:(id)json forView:(viewClass *)view withDefaultView:(viewClass *)defaultView - (void)set_##name:(id)json forView:(viewClass *)view withDefaultView:(viewClass *)defaultView
#define RCT_CUSTOM_SHADOW_PROPERTY(name, type, viewClass) \ #define RCT_CUSTOM_SHADOW_PROPERTY(name, type, viewClass) \
RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ + (NSDictionary *)getPropConfigShadow_##name { return @{@"name": @#name, @"type": @#type}; } \
- (void)set_##name:(id)json forShadowView:(viewClass *)view withDefaultView:(viewClass *)defaultView - (void)set_##name:(id)json forShadowView:(viewClass *)view withDefaultView:(viewClass *)defaultView
/** /**
@ -164,17 +162,4 @@ RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \
[self set_##newName:json forView:view withDefaultView:defaultView]; \ [self set_##newName:json forView:view withDefaultView:defaultView]; \
} }
/**
* PROP_CONFIG macros should only be paired with property setters.
*/
#define RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \
+ (NSDictionary *)getPropConfigView_##name { \
return @{@"name": @#name, @"type": @#type}; \
}
#define RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \
+ (NSDictionary *)getPropConfigShadow_##name { \
return @{@"name": @#name, @"type": @#type}; \
}
@end @end

View File

@ -14,6 +14,7 @@
#import "RCTEventDispatcher.h" #import "RCTEventDispatcher.h"
#import "RCTLog.h" #import "RCTLog.h"
#import "RCTShadowView.h" #import "RCTShadowView.h"
#import "RCTUIManager.h"
#import "RCTUtils.h" #import "RCTUtils.h"
#import "RCTView.h" #import "RCTView.h"
@ -23,6 +24,11 @@
RCT_EXPORT_MODULE() RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return [_bridge.uiManager methodQueue];
}
- (UIView *)view - (UIView *)view
{ {
return [[RCTView alloc] init]; return [[RCTView alloc] init];