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

View File

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

View File

@@ -68,6 +68,11 @@ RCT_EXPORT_MODULE()
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
{
if (count == 1) {

View File

@@ -33,7 +33,9 @@ typedef struct {
CLLocationAccuracy accuracy;
} RCTLocationOptions;
static RCTLocationOptions RCTLocationOptionsWithJSON(id json)
@implementation RCTConvert (RCTLocationOptions)
+ (RCTLocationOptions)RCTLocationOptions:(id)json
{
NSDictionary *options = [RCTConvert NSDictionary:json];
return (RCTLocationOptions){
@@ -43,6 +45,8 @@ static RCTLocationOptions RCTLocationOptionsWithJSON(id json)
};
}
@end
static NSDictionary *RCTPositionError(RCTPositionErrorCode code, NSString *msg /* nil for default */)
{
if (!msg) {
@@ -121,6 +125,7 @@ RCT_EXPORT_MODULE()
- (void)dealloc
{
[_locationManager stopUpdatingLocation];
_locationManager.delegate = nil;
}
#pragma mark - Private API
@@ -153,41 +158,33 @@ RCT_EXPORT_MODULE()
#pragma mark - Public API
RCT_EXPORT_METHOD(startObserving:(NSDictionary *)optionsJSON)
RCT_EXPORT_METHOD(startObserving:(RCTLocationOptions)options)
{
[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
_observerOptions = RCTLocationOptionsWithJSON(optionsJSON);
for (RCTLocationRequest *request in _pendingRequests) {
_observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy);
}
_locationManager.desiredAccuracy = _observerOptions.accuracy;
[self beginLocationUpdates];
_observingLocation = YES;
});
_locationManager.desiredAccuracy = _observerOptions.accuracy;
[self beginLocationUpdates];
_observingLocation = YES;
}
RCT_EXPORT_METHOD(stopObserving)
{
dispatch_async(dispatch_get_main_queue(), ^{
// Stop observing
_observingLocation = NO;
// Stop observing
_observingLocation = NO;
// Stop updating if no pending requests
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
errorCallback:(RCTResponseSenderBlock)errorBlock)
{
@@ -198,56 +195,49 @@ RCT_EXPORT_METHOD(getCurrentPosition:(NSDictionary *)optionsJSON
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (![CLLocationManager locationServicesEnabled]) {
if (errorBlock) {
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]);
if (![CLLocationManager locationServicesEnabled]) {
if (errorBlock) {
errorBlock(@[
RCTPositionError(RCTPositionErrorUnavailable, @"Location services disabled.")
]);
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];
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
if (errorBlock) {
errorBlock(@[
RCTPositionError(RCTPositionErrorDenied, nil)
]);
return;
}
}
// Configure location manager and begin updating location
_locationManager.desiredAccuracy = MIN(_locationManager.desiredAccuracy, options.accuracy);
[self beginLocationUpdates];
// 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;
}
// 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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
@implementation RCTTestRunner
{
FBSnapshotTestController *_snapshotController;
FBSnapshotTestController *_testController;
}
- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir
@@ -27,8 +27,8 @@
if ((self = [super init])) {
NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"];
_snapshotController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName];
_snapshotController.referenceImagesDirectory = referenceDir;
_testController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName];
_testController.referenceImagesDirectory = referenceDir;
_scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]];
}
return self;
@@ -36,12 +36,12 @@
- (void)setRecordMode:(BOOL)recordMode
{
_snapshotController.recordMode = recordMode;
_testController.recordMode = recordMode;
}
- (BOOL)recordMode
{
return _snapshotController.recordMode;
return _testController.recordMode;
}
- (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
{
UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
if ([vc.view isKindOfClass:[RCTRootView class]]) {
[(RCTRootView *)vc.view invalidate]; // Make sure the normal app view doesn't interfere
}
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
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:_scriptURL
moduleName:moduleName
launchOptions:nil];
rootView.initialProperties = initialProps;
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];
NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage];
while ([date timeIntervalSinceNow] > 0 && ![testModule isDone] && error == nil) {