Initial commit

This commit is contained in:
Ben Alpert
2015-01-29 17:10:49 -08:00
commit a15603d8f1
382 changed files with 39183 additions and 0 deletions

36
ReactKit/Base/RCTAssert.h Normal file
View File

@@ -0,0 +1,36 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <Foundation/Foundation.h>
#define RCTErrorDomain @"RCTErrorDomain"
#define RCTAssert(condition, message, ...) _RCTAssert(condition, message, ##__VA_ARGS__)
#define RCTCAssert(condition, message, ...) _RCTCAssert(condition, message, ##__VA_ARGS__)
typedef void (^RCTAssertFunction)(BOOL condition, NSString *message, ...);
extern RCTAssertFunction RCTInjectedAssertFunction;
extern RCTAssertFunction RCTInjectedCAssertFunction;
void RCTInjectAssertFunctions(RCTAssertFunction assertFunction, RCTAssertFunction cAssertFunction);
#define _RCTAssert(condition, message, ...) \
do { \
if (RCTInjectedAssertFunction) { \
RCTInjectedAssertFunction(condition, message, ##__VA_ARGS__); \
} else { \
NSAssert(condition, message, ##__VA_ARGS__); \
} \
} while (false)
#define _RCTCAssert(condition, message, ...) \
do { \
if (RCTInjectedCAssertFunction) { \
RCTInjectedCAssertFunction(condition, message, ##__VA_ARGS__); \
} else { \
NSCAssert(condition, message, ##__VA_ARGS__); \
} \
} while (false)
#define RCTAssertMainThread() RCTAssert([NSThread isMainThread], @"This method must be called on the main thread");
#define RCTCAssertMainThread() RCTCAssert([NSThread isMainThread], @"This function must be called on the main thread");

12
ReactKit/Base/RCTAssert.m Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTAssert.h"
RCTAssertFunction RCTInjectedAssertFunction = nil;
RCTAssertFunction RCTInjectedCAssertFunction = nil;
void RCTInjectAssertFunctions(RCTAssertFunction assertFunction, RCTAssertFunction cAssertFunction)
{
RCTInjectedAssertFunction = assertFunction;
RCTInjectedCAssertFunction = cAssertFunction;
}

View File

@@ -0,0 +1,13 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
/**
* Defines a View that wants to support auto insets adjustment
*/
@protocol RCTAutoInsetsProtocol
@property (nonatomic, assign, readwrite) UIEdgeInsets contentInset;
@property (nonatomic, assign, readwrite) BOOL automaticallyAdjustContentInsets;
@end

85
ReactKit/Base/RCTBridge.h Normal file
View File

@@ -0,0 +1,85 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTExport.h"
#import "RCTInvalidating.h"
#import "RCTJavaScriptExecutor.h"
@protocol RCTNativeModule;
@class RCTUIManager;
@class RCTJavaScriptEventDispatcher;
/**
* Functions are the one thing that aren't automatically converted to OBJC
* blocks, according to this revert: http://trac.webkit.org/changeset/144489
* They must be expressed as `JSValue`s.
*
* But storing callbacks causes reference cycles!
* http://stackoverflow.com/questions/19202248/how-can-i-use-jsmanagedvalue-to-avoid-a-reference-cycle-without-the-jsvalue-gett
* We'll live with the leak for now, but need to clean this up asap:
* Passing a reference to the `context` to the bridge would make it easy to
* execute JS. We can add `JSManagedValue`s to protect against this. The same
* needs to be done in `RCTTiming` and friends.
*/
/**
* Must be kept in sync with `MessageQueue.js`.
*/
typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
RCTBridgeFieldRequestModuleIDs = 0,
RCTBridgeFieldMethodIDs,
RCTBridgeFieldParamss,
RCTBridgeFieldResponseCBIDs,
RCTBridgeFieldResponseReturnValues,
RCTBridgeFieldFlushDateMillis
};
/**
* Utilities for constructing common response objects. When sending a
* systemError back to JS, it's important to describe whether or not it was a
* system error, or API usage error. System errors should never happen and are
* therefore logged using `RCTLogError()`. API usage errors are expected if the
* API is misused and will therefore not be logged using `RCTLogError()`. The JS
* application code is expected to handle them. Regardless of type, each error
* should be logged at most once.
*/
static inline NSDictionary *RCTSystemErrorObject(NSString *msg)
{
return @{@"systemError": msg ?: @""};
}
static inline NSDictionary *RCTAPIErrorObject(NSString *msg)
{
return @{@"apiError": msg ?: @""};
}
/**
* Async batched bridge used to communicate with `RCTJavaScriptAppEngine`.
*/
@interface RCTBridge : NSObject <RCTInvalidating>
- (instancetype)initWithJavaScriptExecutor:(id<RCTJavaScriptExecutor>)javaScriptExecutor
shadowQueue:(dispatch_queue_t)shadowQueue
javaScriptModulesConfig:(NSDictionary *)javaScriptModulesConfig;
- (void)enqueueJSCall:(NSUInteger)moduleID methodID:(NSUInteger)methodID args:(NSArray *)args;
- (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete;
- (void)enqueueUpdateTimers;
@property (nonatomic, readonly) RCTUIManager *uiManager;
@property (nonatomic, readonly) RCTJavaScriptEventDispatcher *eventDispatcher;
// For use in implementing delegates, which may need to queue responses.
- (RCTResponseSenderBlock)createResponseSenderBlock:(NSInteger)callbackID;
/**
* Global logging function will print to both xcode and js debugger consoles.
*
* NOTE: Use via RCTLog* macros defined in RCTLog.h
* TODO (#5906496): should log function be exposed here, or could it be a module?
*/
+ (void)log:(NSArray *)objects level:(NSString *)level;
+ (BOOL)hasValidJSExecutor;
@end

515
ReactKit/Base/RCTBridge.m Normal file
View File

@@ -0,0 +1,515 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTBridge.h"
#import <objc/message.h>
#import "RCTModuleMethod.h"
#import "RCTInvalidating.h"
#import "RCTJavaScriptEventDispatcher.h"
#import "RCTLog.h"
#import "RCTModuleIDs.h"
#import "RCTTiming.h"
#import "RCTUIManager.h"
#import "RCTUtils.h"
NSString *RCTModuleName(Class moduleClass)
{
if ([moduleClass respondsToSelector:@selector(moduleName)]) {
return [moduleClass moduleName];
} else {
// Default implementation, works in most cases
NSString *className = NSStringFromClass(moduleClass);
// TODO: be more consistent with naming so that this check isn't needed
if ([moduleClass conformsToProtocol:@protocol(RCTNativeViewModule)]) {
if ([className hasPrefix:@"RCTUI"]) {
className = [className substringFromIndex:@"RCT".length];
}
if ([className hasSuffix:@"Manager"]) {
className = [className substringToIndex:className.length - @"Manager".length];
}
}
return className;
}
}
NSDictionary *RCTNativeModuleClasses(void)
{
static NSMutableDictionary *modules;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
modules = [NSMutableDictionary dictionary];
unsigned int classCount;
Class *classes = objc_copyClassList(&classCount);
for (unsigned int i = 0; i < classCount; i++) {
Class cls = classes[i];
if (!class_getSuperclass(cls)) {
// Class has no superclass - it's probably something weird
continue;
}
if (![cls conformsToProtocol:@protocol(RCTNativeModule)]) {
// Not an RCTNativeModule
continue;
}
// Get module name
NSString *moduleName = RCTModuleName(cls);
// Check module name is unique
id existingClass = modules[moduleName];
RCTCAssert(existingClass == Nil, @"Attempted to register RCTNativeModule class %@ for the name '%@', but name was already registered by class %@", cls, moduleName, existingClass);
modules[moduleName] = cls;
}
free(classes);
});
return modules;
}
@implementation RCTBridge
{
NSMutableDictionary *_moduleInstances;
NSDictionary *_javaScriptModulesConfig;
dispatch_queue_t _shadowQueue;
RCTTiming *_timing;
id<RCTJavaScriptExecutor> _javaScriptExecutor;
}
static id<RCTJavaScriptExecutor> _latestJSExecutor;
- (instancetype)initWithJavaScriptExecutor:(id<RCTJavaScriptExecutor>)javaScriptExecutor
shadowQueue:(dispatch_queue_t)shadowQueue
javaScriptModulesConfig:(NSDictionary *)javaScriptModulesConfig
{
if ((self = [super init])) {
_javaScriptExecutor = javaScriptExecutor;
_latestJSExecutor = _javaScriptExecutor;
_shadowQueue = shadowQueue;
_eventDispatcher = [[RCTJavaScriptEventDispatcher alloc] initWithBridge:self];
_moduleInstances = [[NSMutableDictionary alloc] init];
// TODO (#5906496): Remove special case
_timing = [[RCTTiming alloc] initWithBridge:self];
_javaScriptModulesConfig = javaScriptModulesConfig;
_moduleInstances[RCTModuleName([RCTTiming class])] = _timing;
// TODO (#5906496): Remove special case
NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init];
[RCTNativeModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) {
if ([moduleClass conformsToProtocol:@protocol(RCTNativeViewModule)]) {
viewManagers[moduleName] = [[moduleClass alloc] init];
}
}];
_uiManager = [[RCTUIManager alloc] initWithShadowQueue:_shadowQueue viewManagers:viewManagers];
_uiManager.eventDispatcher = _eventDispatcher;
_moduleInstances[RCTModuleName([RCTUIManager class])] = _uiManager;
[_moduleInstances addEntriesFromDictionary:viewManagers];
// Register remaining modules
[RCTNativeModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) {
if (_moduleInstances[moduleName] == nil) {
_moduleInstances[moduleName] = [[moduleClass alloc] init];
}
}];
[self doneRegisteringModules];
}
return self;
}
- (void)dealloc
{
RCTAssert(!self.valid, @"must call -invalidate before -dealloc; TODO: why not call it here then?");
}
#pragma mark - RCTInvalidating
- (BOOL)isValid
{
return _javaScriptExecutor != nil;
}
- (void)invalidate
{
if (_latestJSExecutor == _javaScriptExecutor) {
_latestJSExecutor = nil;
}
_javaScriptExecutor = nil;
dispatch_sync(_shadowQueue, ^{
// Make sure all dispatchers have been executed before
// freeing up memory from _asyncHookMapByModuleID
});
for (id target in _moduleInstances.objectEnumerator) {
if ([target respondsToSelector:@selector(invalidate)]) {
[(id<RCTInvalidating>)target invalidate];
}
}
[_moduleInstances removeAllObjects];
_timing = nil;
}
/**
* - TODO (#5906496): When we build a `MessageQueue.m`, handling all the requests could
* cause both a queue of "responses". We would flush them here. However, we
* currently just expect each objc block to handle its own response sending
* using a `RCTResponseSenderBlock`.
*/
#pragma mark - RCTBridge methods
/**
* Like JS::call, for objective-c.
*/
- (void)enqueueJSCall:(NSUInteger)moduleID methodID:(NSUInteger)methodID args:(NSArray *)args
{
RCTAssertMainThread();
[self _invokeRemoteJSModule:moduleID methodID:methodID args:args];
}
- (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");
[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {
if (scriptLoadError) {
onComplete(scriptLoadError);
return;
}
[_javaScriptExecutor executeJSCall:@"BatchedBridge"
method:@"flushedQueue"
arguments:@[]
callback:^(id objcValue, NSError *error) {
[self _handleBuffer:objcValue];
onComplete(error);
}];
}];
}
- (void)enqueueUpdateTimers
{
[_timing enqueueUpdateTimers];
}
#pragma mark - Payload Generation
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args
{
NSTimeInterval startJS = RCTTGetAbsoluteTime();
RCTJavaScriptCallback processResponse = ^(id objcValue, NSError *error) {
NSTimeInterval startNative = RCTTGetAbsoluteTime();
[self _handleBuffer:objcValue];
NSTimeInterval end = RCTTGetAbsoluteTime();
NSTimeInterval timeJS = startNative - startJS;
NSTimeInterval timeNative = end - startNative;
// TODO: surface this performance information somewhere
[[NSNotificationCenter defaultCenter] postNotificationName:@"PERF" object:nil userInfo:@{@"JS": @(timeJS * 1000000), @"Native": @(timeNative * 1000000)}];
};
[_javaScriptExecutor executeJSCall:module
method:method
arguments:args
callback:processResponse];
}
- (void)_invokeRemoteJSModule:(NSUInteger)moduleID methodID:(NSUInteger)methodID args:(NSArray *)args
{
[self _invokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue"
arguments:@[@(moduleID), @(methodID), args]];
}
/**
* TODO (#5906496): Have responses piggy backed on a round trip with ObjC->JS requests.
*/
- (void)_sendResponseToJavaScriptCallbackID:(NSInteger)cbID args:(NSArray *)args
{
[self _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[@(cbID), args]];
}
#pragma mark - Payload Processing
- (void)_handleBuffer:(id)buffer
{
if (buffer == nil || buffer == (id)kCFNull) {
return;
}
if (![buffer isKindOfClass:[NSArray class]]) {
RCTLogMustFix(@"Buffer must be an instance of NSArray, got %@", NSStringFromClass([buffer class]));
return;
}
NSArray *requestsArray = (NSArray *)buffer;
NSUInteger bufferRowCount = [requestsArray count];
NSUInteger expectedFieldsCount = RCTBridgeFieldResponseReturnValues + 1;
if (bufferRowCount != expectedFieldsCount) {
RCTLogMustFix(@"Must pass all fields to buffer - expected %zd, saw %zd", expectedFieldsCount, bufferRowCount);
return;
}
for (NSUInteger fieldIndex = RCTBridgeFieldRequestModuleIDs; fieldIndex <= RCTBridgeFieldParamss; fieldIndex++) {
id field = [requestsArray objectAtIndex:fieldIndex];
if (![field isKindOfClass:[NSArray class]]) {
RCTLogMustFix(@"Field at index %zd in buffer must be an instance of NSArray, got %@", fieldIndex, NSStringFromClass([field class]));
return;
}
}
NSArray *moduleIDs = [requestsArray objectAtIndex:RCTBridgeFieldRequestModuleIDs];
NSArray *methodIDs = [requestsArray objectAtIndex:RCTBridgeFieldMethodIDs];
NSArray *paramss = [requestsArray objectAtIndex:RCTBridgeFieldParamss];
NSUInteger numRequests = [moduleIDs count];
BOOL allSame = numRequests == [methodIDs count] && numRequests == [paramss count];
if (!allSame) {
RCTLogMustFix(@"Invalid data message - all must be length: %zd", numRequests);
return;
}
for (NSUInteger i = 0; i < numRequests; i++) {
@autoreleasepool {
[self _handleRequestNumber:i
moduleID:[moduleIDs objectAtIndex:i]
methodID:[methodIDs objectAtIndex:i]
params:[paramss objectAtIndex:i]];
}
}
// Update modules
for (id target in _moduleInstances.objectEnumerator) {
if ([target respondsToSelector:@selector(batchDidComplete)]) {
dispatch_async(_shadowQueue, ^{
[target batchDidComplete];
});
}
}
}
- (void)_handleRequestNumber:(NSUInteger)i moduleID:(id)moduleID methodID:(id)methodID params:(id)params
{
if (![moduleID isKindOfClass:[NSNumber class]] || ![methodID isKindOfClass:[NSNumber class]] || ![params isKindOfClass:[NSArray class]]) {
RCTLogMustFix(@"Invalid module/method/params tuple for request #%zd", i);
return;
}
[self _dispatchUsingAsyncHookMapWithModuleID:[moduleID integerValue]
methodID:[methodID integerValue]
params:params];
}
/**
* Returns a callback that reports values back to the JS thread.
* TODO (#5906496): These responses should go into their own queue `MessageQueue.m` that
* mirrors the JS queue and protocol. For now, we speak the "language" of the JS
* queue by packing it into an array that matches the wire protocol.
*/
- (RCTResponseSenderBlock)createResponseSenderBlock:(NSInteger)cbID
{
if (!cbID) {
return nil;
}
return ^(NSArray *args) {
[self _sendResponseToJavaScriptCallbackID:cbID args:args];
};
}
+ (NSInvocation *)invocationForAdditionalArguments:(NSUInteger)argCount
{
static NSMutableDictionary *invocations;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
invocations = [NSMutableDictionary dictionary];
});
id key = @(argCount);
NSInvocation *invocation = invocations[key];
if (invocation == nil) {
NSString *objCTypes = [@"v@:" stringByPaddingToLength:3 + argCount withString:@"@" startingAtIndex:0];
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:objCTypes.UTF8String];
invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocations[key] = invocation;
}
return invocation;
}
- (BOOL)_dispatchUsingAsyncHookMapWithModuleID:(NSInteger)moduleID
methodID:(NSInteger)methodID
params:(NSArray *)params
{
if (moduleID < 0 || moduleID >= RCTExportedMethodsByModule().count) {
return NO;
}
NSString *moduleName = RCTExportedModuleNameAtSortedIndex(moduleID);
NSArray *methods = RCTExportedMethodsByModule()[moduleName];
if (methodID < 0 || methodID >= methods.count) {
return NO;
}
RCTModuleMethod *method = methods[methodID];
NSUInteger methodArity = method.arity;
if (params.count != methodArity) {
RCTLogMustFix(
@"Expected %tu arguments but got %tu invoking %@.%@",
methodArity,
params.count,
moduleName,
method.JSMethodName
);
return NO;
}
__weak RCTBridge *weakSelf = self;
dispatch_async(_shadowQueue, ^{
__strong RCTBridge *strongSelf = weakSelf;
if (!strongSelf.isValid) {
// strongSelf has been invalidated since the dispatch_async call and this
// invocation should not continue.
return;
}
NSInvocation *invocation = [RCTBridge invocationForAdditionalArguments:methodArity];
// TODO: we should just store module instances by index, since that's how we look them up anyway
id target = strongSelf->_moduleInstances[moduleName];
RCTAssert(target != nil, @"No module found for name '%@'", moduleName);
[invocation setArgument:&target atIndex:0];
SEL selector = method.selector;
[invocation setArgument:&selector atIndex:1];
// Retain used blocks until after invocation completes.
NSMutableArray *blocks = [NSMutableArray array];
[params enumerateObjectsUsingBlock:^(id param, NSUInteger idx, BOOL *stop) {
if ([param isEqual:[NSNull null]]) {
param = nil;
} else if ([method.blockArgumentIndexes containsIndex:idx]) {
id block = [strongSelf createResponseSenderBlock:[param integerValue]];
[blocks addObject:block];
param = block;
}
[invocation setArgument:&param atIndex:idx + 2];
}];
@try {
[invocation invoke];
}
@catch (NSException *exception) {
RCTLogMustFix(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, target, params, exception);
}
@finally {
// Force `blocks` to remain alive until here.
blocks = nil;
}
});
return YES;
}
- (void)doneRegisteringModules
{
RCTAssertMainThread();
RCTAssert(_javaScriptModulesConfig != nil, @"JS module config not loaded in APP");
NSMutableDictionary *objectsToInject = [NSMutableDictionary dictionary];
// Dictionary of { moduleName0: { moduleID: 0, methods: { methodName0: { methodID: 0, type: remote }, methodName1: { ... }, ... }, ... }
NSUInteger moduleCount = RCTExportedMethodsByModule().count;
NSMutableDictionary *moduleConfigs = [NSMutableDictionary dictionaryWithCapacity:RCTExportedMethodsByModule().count];
for (NSUInteger i = 0; i < moduleCount; i++) {
NSString *moduleName = RCTExportedModuleNameAtSortedIndex(i);
NSArray *rawMethods = RCTExportedMethodsByModule()[moduleName];
NSMutableDictionary *methods = [NSMutableDictionary dictionaryWithCapacity:rawMethods.count];
[rawMethods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *stop) {
methods[method.JSMethodName] = @{
@"methodID": @(methodID),
@"type": @"remote",
};
}];
NSMutableDictionary *moduleConfig = [NSMutableDictionary dictionary];
moduleConfig[@"moduleID"] = @(i);
moduleConfig[@"methods"] = methods;
id target = [_moduleInstances objectForKey:moduleName];
if ([target respondsToSelector:@selector(constantsToExport)] && ![target conformsToProtocol:@protocol(RCTNativeViewModule)]) {
// TODO: find a more elegant way to handle RCTNativeViewModule constants as a special case
moduleConfig[@"constants"] = [target constantsToExport];
}
moduleConfigs[moduleName] = moduleConfig;
}
NSDictionary *batchedBridgeConfig = @{
@"remoteModuleConfig": moduleConfigs,
@"localModulesConfig": _javaScriptModulesConfig
};
NSString *configJSON = RCTJSONStringify(batchedBridgeConfig, NULL);
objectsToInject[@"__fbBatchedBridgeConfig"] = configJSON;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[objectsToInject enumerateKeysAndObjectsUsingBlock:^(NSString *objectName, NSString *script, BOOL *stop) {
[_javaScriptExecutor injectJSONText:script asGlobalObjectNamed:objectName callback:^(id err) {
dispatch_semaphore_signal(semaphore);
}];
}];
for (NSUInteger i = 0, count = objectsToInject.count; i < count; i++) {
if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) {
RCTLogMustFix(@"JavaScriptExecutor take too long to inject JSON object");
}
}
}
+ (BOOL)hasValidJSExecutor
{
return (_latestJSExecutor != nil && [_latestJSExecutor isValid]);
}
+ (void)log:(NSArray *)objects level:(NSString *)level
{
if (!_latestJSExecutor || ![_latestJSExecutor isValid]) {
RCTLogError(@"%@", RCTLogFormatString(@"ERROR: No valid JS executor to log %@.", objects));
return;
}
NSMutableArray *args = [NSMutableArray arrayWithObject:level];
// TODO (#5906496): Find out and document why we skip the first object
for (id ob in [objects subarrayWithRange:(NSRange){1, [objects count] - 1}]) {
if ([NSJSONSerialization isValidJSONObject:@[ob]]) {
[args addObject:ob];
} else {
[args addObject:[ob description]];
}
}
// Note the js executor could get invalidated while we're trying to call this...need to watch out for that.
[_latestJSExecutor executeJSCall:@"RCTLog"
method:@"logIfNoNativeHook"
arguments:args
callback:^(id objcValue, NSError *error) {}];
}
@end

View File

@@ -0,0 +1,59 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIKit.h>
#import "Layout.h"
@interface RCTConvert : NSObject
+ (BOOL)BOOL:(id)json;
+ (double)double:(id)json;
+ (float)float:(id)json;
+ (int)int:(id)json;
+ (NSString *)NSString:(id)json;
+ (NSNumber *)NSNumber:(id)json;
+ (NSInteger)NSInteger:(id)json;
+ (NSUInteger)NSUInteger:(id)json;
+ (NSURL *)NSURL:(id)json;
+ (NSURLRequest *)NSURLRequest:(id)json;
+ (NSDate *)NSDate:(id)json;
+ (NSTimeZone *)NSTimeZone:(id)json;
+ (NSTimeInterval)NSTimeInterval:(id)json;
+ (NSTextAlignment)NSTextAlignment:(id)json;
+ (NSWritingDirection)NSWritingDirection:(id)json;
+ (UITextAutocapitalizationType)UITextAutocapitalizationType:(id)json;
+ (UIKeyboardType)UIKeyboardType:(id)json;
+ (CGFloat)CGFloat:(id)json;
+ (CGPoint)CGPoint:(id)json;
+ (CGSize)CGSize:(id)json;
+ (CGRect)CGRect:(id)json;
+ (UIEdgeInsets)UIEdgeInsets:(id)json;
+ (CATransform3D)CATransform3D:(id)json;
+ (CGAffineTransform)CGAffineTransform:(id)json;
+ (UIColor *)UIColor:(id)json;
+ (CGColorRef)CGColor:(id)json;
+ (UIImage *)UIImage:(id)json;
+ (CGImageRef)CGImage:(id)json;
+ (UIFont *)UIFont:(UIFont *)font withSize:(id)json;
+ (UIFont *)UIFont:(UIFont *)font withWeight:(id)json;
+ (UIFont *)UIFont:(UIFont *)font withFamily:(id)json;
+ (UIFont *)UIFont:(UIFont *)font withFamily:(id)json size:(id)json weight:(id)json;
+ (BOOL)css_overflow:(id)json;
+ (css_flex_direction_t)css_flex_direction_t:(id)json;
+ (css_justify_t)css_justify_t:(id)json;
+ (css_align_t)css_align_t:(id)json;
+ (css_position_type_t)css_position_type_t:(id)json;
+ (css_wrap_type_t)css_wrap_type_t:(id)json;
@end

574
ReactKit/Base/RCTConvert.m Normal file
View File

@@ -0,0 +1,574 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTConvert.h"
#import "RCTLog.h"
CGFloat const RCTDefaultFontSize = 14;
NSString *const RCTDefaultFontName = @"HelveticaNeue";
NSString *const RCTDefaultFontWeight = @"normal";
NSString *const RCTBoldFontWeight = @"bold";
#define RCT_CONVERTER_CUSTOM(type, name, code) \
+ (type)name:(id)json \
{ \
@try { \
return code; \
} \
@catch (__unused NSException *e) { \
RCTLogMustFix(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \
json, [json class], #type); \
json = nil; \
return code; \
} \
}
#define RCT_CONVERTER(type, name, getter) \
RCT_CONVERTER_CUSTOM(type, name, [json getter])
#define RCT_ENUM_CONVERTER(type, values, default, getter) \
+ (type)type:(id)json \
{ \
static NSDictionary *mapping; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
mapping = values; \
}); \
if (!json) { \
return default; \
} \
if ([json isKindOfClass:[NSNumber class]]) { \
if ([[mapping allValues] containsObject:json]) { \
return [json getter]; \
} \
RCTLogMustFix(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allValues]); \
return default; \
} \
if (![json isKindOfClass:[NSString class]]) { \
RCTLogMustFix(@"Expected NSNumber or NSString for %s, received %@: %@", #type, [json class], json); \
} \
id value = mapping[json]; \
if(!value && [json description].length > 0) { \
RCTLogMustFix(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allKeys]); \
} \
return value ? [value getter] : default; \
}
#define RCT_STRUCT_CONVERTER(type, values) \
+ (type)type:(id)json \
{ \
@try { \
static NSArray *fields; \
static NSUInteger count; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
fields = values; \
count = [fields count]; \
}); \
type result; \
if ([json isKindOfClass:[NSArray class]]) { \
if ([json count] != count) { \
RCTLogMustFix(@"Expected array with count %zd, but count is %zd: %@", count, [json count], json); \
} else { \
for (NSUInteger i = 0; i < count; i++) { \
((CGFloat *)&result)[i] = [json[i] doubleValue]; \
} \
} \
} else { \
if (![json isKindOfClass:[NSDictionary class]]) { \
RCTLogMustFix(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \
} else { \
for (NSUInteger i = 0; i < count; i++) { \
((CGFloat *)&result)[i] = [json[fields[i]] doubleValue]; \
} \
} \
} \
return result; \
} \
@catch (__unused NSException *e) { \
RCTLogMustFix(@"JSON value '%@' cannot be converted to '%s'", json, #type); \
type result; \
return result; \
} \
}
@implementation RCTConvert
RCT_CONVERTER(BOOL, BOOL, boolValue)
RCT_CONVERTER(double, double, doubleValue)
RCT_CONVERTER(float, float, floatValue)
RCT_CONVERTER(int, int, intValue)
RCT_CONVERTER(NSString *, NSString, description)
RCT_CONVERTER_CUSTOM(NSNumber *, NSNumber, @([json doubleValue]))
RCT_CONVERTER(NSInteger, NSInteger, integerValue)
RCT_CONVERTER_CUSTOM(NSUInteger, NSUInteger, [json unsignedIntegerValue])
+ (NSURL *)NSURL:(id)json
{
if (![json isKindOfClass:[NSString class]]) {
RCTLogMustFix(@"Expected NSString for NSURL, received %@: %@", [json class], json);
return nil;
}
NSString *path = json;
if ([path isAbsolutePath])
{
return [NSURL fileURLWithPath:path];
}
else if ([path length])
{
return [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]];
}
return nil;
}
+ (NSURLRequest *)NSURLRequest:(id)json
{
return [NSURLRequest requestWithURL:[self NSURL:json]];
}
RCT_CONVERTER_CUSTOM(NSDate *, NSDate, [NSDate dateWithTimeIntervalSince1970:[json doubleValue]])
RCT_CONVERTER_CUSTOM(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[json doubleValue]])
RCT_CONVERTER(NSTimeInterval, NSTimeInterval, doubleValue)
/**
* NOTE: We don't deliberately don't support NSTextAlignmentJustified in the
* X-platform RCTText implementation because it isn't available on Android.
* We may wish to support this for iOS-specific controls such as UILabel.
*/
RCT_ENUM_CONVERTER(NSTextAlignment, (@{
@"auto": @(NSTextAlignmentNatural),
@"left": @(NSTextAlignmentLeft),
@"center": @(NSTextAlignmentCenter),
@"right": @(NSTextAlignmentRight),
/* @"justify": @(NSTextAlignmentJustify), */
}), NSTextAlignmentNatural, integerValue)
RCT_ENUM_CONVERTER(NSWritingDirection, (@{
@"auto": @(NSWritingDirectionNatural),
@"ltr": @(NSWritingDirectionLeftToRight),
@"rtl": @(NSWritingDirectionRightToLeft),
}), NSWritingDirectionNatural, integerValue)
RCT_ENUM_CONVERTER(UITextAutocapitalizationType, (@{
@"none": @(UITextAutocapitalizationTypeNone),
@"words": @(UITextAutocapitalizationTypeWords),
@"sentences": @(UITextAutocapitalizationTypeSentences),
@"all": @(UITextAutocapitalizationTypeAllCharacters)
}), UITextAutocapitalizationTypeSentences, integerValue)
RCT_ENUM_CONVERTER(UIKeyboardType, (@{
@"numeric": @(UIKeyboardTypeDecimalPad),
@"default": @(UIKeyboardTypeDefault),
}), UIKeyboardTypeDefault, integerValue)
RCT_CONVERTER(CGFloat, CGFloat, doubleValue)
RCT_STRUCT_CONVERTER(CGPoint, (@[@"x", @"y"]))
RCT_STRUCT_CONVERTER(CGSize, (@[@"w", @"h"]))
RCT_STRUCT_CONVERTER(CGRect, (@[@"x", @"y", @"w", @"h"]))
RCT_STRUCT_CONVERTER(UIEdgeInsets, (@[@"top", @"left", @"bottom", @"right"]))
RCT_STRUCT_CONVERTER(CATransform3D, (@[
@"m11", @"m12", @"m13", @"m14",
@"m21", @"m22", @"m23", @"m24",
@"m31", @"m32", @"m33", @"m34",
@"m41", @"m42", @"m43", @"m44"
]))
RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"]))
+ (UIColor *)UIColor:(id)json
{
// Check color cache
static NSMutableDictionary *colorCache = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
colorCache = [[NSMutableDictionary alloc] init];
});
UIColor *color = colorCache[json];
if (color) {
return color;
}
if ([json isKindOfClass:[NSString class]]) {
// Check named colors
static NSDictionary *namedColors = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
namedColors = @{
// CSS colors
@"aliceblue": @"#f0f8ff",
@"antiquewhite": @"#faebd7",
@"aqua": @"#00ffff",
@"aquamarine": @"#7fffd4",
@"azure": @"#f0ffff",
@"beige": @"#f5f5dc",
@"bisque": @"#ffe4c4",
@"black": @"#000000",
@"blanchedalmond": @"#ffebcd",
@"blue": @"#0000ff",
@"blueviolet": @"#8a2be2",
@"brown": @"#a52a2a",
@"burlywood": @"#deb887",
@"cadetblue": @"#5f9ea0",
@"chartreuse": @"#7fff00",
@"chocolate": @"#d2691e",
@"coral": @"#ff7f50",
@"cornflowerblue": @"#6495ed",
@"cornsilk": @"#fff8dc",
@"crimson": @"#dc143c",
@"cyan": @"#00ffff",
@"darkblue": @"#00008b",
@"darkcyan": @"#008b8b",
@"darkgoldenrod": @"#b8860b",
@"darkgray": @"#a9a9a9",
@"darkgrey": @"#a9a9a9",
@"darkgreen": @"#006400",
@"darkkhaki": @"#bdb76b",
@"darkmagenta": @"#8b008b",
@"darkolivegreen": @"#556b2f",
@"darkorange": @"#ff8c00",
@"darkorchid": @"#9932cc",
@"darkred": @"#8b0000",
@"darksalmon": @"#e9967a",
@"darkseagreen": @"#8fbc8f",
@"darkslateblue": @"#483d8b",
@"darkslategray": @"#2f4f4f",
@"darkslategrey": @"#2f4f4f",
@"darkturquoise": @"#00ced1",
@"darkviolet": @"#9400d3",
@"deeppink": @"#ff1493",
@"deepskyblue": @"#00bfff",
@"dimgray": @"#696969",
@"dimgrey": @"#696969",
@"dodgerblue": @"#1e90ff",
@"firebrick": @"#b22222",
@"floralwhite": @"#fffaf0",
@"forestgreen": @"#228b22",
@"fuchsia": @"#ff00ff",
@"gainsboro": @"#dcdcdc",
@"ghostwhite": @"#f8f8ff",
@"gold": @"#ffd700",
@"goldenrod": @"#daa520",
@"gray": @"#808080",
@"grey": @"#808080",
@"green": @"#008000",
@"greenyellow": @"#adff2f",
@"honeydew": @"#f0fff0",
@"hotpink": @"#ff69b4",
@"indianred": @"#cd5c5c",
@"indigo": @"#4b0082",
@"ivory": @"#fffff0",
@"khaki": @"#f0e68c",
@"lavender": @"#e6e6fa",
@"lavenderblush": @"#fff0f5",
@"lawngreen": @"#7cfc00",
@"lemonchiffon": @"#fffacd",
@"lightblue": @"#add8e6",
@"lightcoral": @"#f08080",
@"lightcyan": @"#e0ffff",
@"lightgoldenrodyellow": @"#fafad2",
@"lightgray": @"#d3d3d3",
@"lightgrey": @"#d3d3d3",
@"lightgreen": @"#90ee90",
@"lightpink": @"#ffb6c1",
@"lightsalmon": @"#ffa07a",
@"lightseagreen": @"#20b2aa",
@"lightskyblue": @"#87cefa",
@"lightslategray": @"#778899",
@"lightslategrey": @"#778899",
@"lightsteelblue": @"#b0c4de",
@"lightyellow": @"#ffffe0",
@"lime": @"#00ff00",
@"limegreen": @"#32cd32",
@"linen": @"#faf0e6",
@"magenta": @"#ff00ff",
@"maroon": @"#800000",
@"mediumaquamarine": @"#66cdaa",
@"mediumblue": @"#0000cd",
@"mediumorchid": @"#ba55d3",
@"mediumpurple": @"#9370db",
@"mediumseagreen": @"#3cb371",
@"mediumslateblue": @"#7b68ee",
@"mediumspringgreen": @"#00fa9a",
@"mediumturquoise": @"#48d1cc",
@"mediumvioletred": @"#c71585",
@"midnightblue": @"#191970",
@"mintcream": @"#f5fffa",
@"mistyrose": @"#ffe4e1",
@"moccasin": @"#ffe4b5",
@"navajowhite": @"#ffdead",
@"navy": @"#000080",
@"oldlace": @"#fdf5e6",
@"olive": @"#808000",
@"olivedrab": @"#6b8e23",
@"orange": @"#ffa500",
@"orangered": @"#ff4500",
@"orchid": @"#da70d6",
@"palegoldenrod": @"#eee8aa",
@"palegreen": @"#98fb98",
@"paleturquoise": @"#afeeee",
@"palevioletred": @"#db7093",
@"papayawhip": @"#ffefd5",
@"peachpuff": @"#ffdab9",
@"peru": @"#cd853f",
@"pink": @"#ffc0cb",
@"plum": @"#dda0dd",
@"powderblue": @"#b0e0e6",
@"purple": @"#800080",
@"rebeccapurple": @"#663399",
@"red": @"#ff0000",
@"rosybrown": @"#bc8f8f",
@"royalblue": @"#4169e1",
@"saddlebrown": @"#8b4513",
@"salmon": @"#fa8072",
@"sandybrown": @"#f4a460",
@"seagreen": @"#2e8b57",
@"seashell": @"#fff5ee",
@"sienna": @"#a0522d",
@"silver": @"#c0c0c0",
@"skyblue": @"#87ceeb",
@"slateblue": @"#6a5acd",
@"slategray": @"#708090",
@"slategrey": @"#708090",
@"snow": @"#fffafa",
@"springgreen": @"#00ff7f",
@"steelblue": @"#4682b4",
@"tan": @"#d2b48c",
@"teal": @"#008080",
@"thistle": @"#d8bfd8",
@"tomato": @"#ff6347",
@"turquoise": @"#40e0d0",
@"violet": @"#ee82ee",
@"wheat": @"#f5deb3",
@"white": @"#ffffff",
@"whitesmoke": @"#f5f5f5",
@"yellow": @"#ffff00",
@"yellowgreen": @"#9acd32",
// Nonstandard color extensions
@"transparent": @"rgba(0,0,0,0)",
@"clear": @"rgba(0,0,0,0)",
};
});
NSString *colorString = namedColors[json];
if (!colorString) {
colorString = json;
}
// Parse color
NSUInteger red = -1;
NSUInteger green = -1;
NSUInteger blue = -1;
CGFloat alpha = 1.0;
if ([colorString hasPrefix:@"#"]) {
sscanf([colorString UTF8String], "#%02tX%02tX%02tX", &red, &green, &blue);
} else if ([colorString hasPrefix:@"rgba("]) {
double tmpAlpha;
sscanf([colorString UTF8String], "rgba(%zd,%zd,%zd,%lf)", &red, &green, &blue, &tmpAlpha);
alpha = tmpAlpha > 0.99 ? 1.0 : tmpAlpha;
} else if ([colorString hasPrefix:@"rgb("]) {
sscanf([colorString UTF8String], "rgb(%zd,%zd,%zd)", &red, &green, &blue);
} else {
RCTLogMustFix(@"Unrecognized color format '%@', must be one of #hex|rgba|rgb", colorString);
}
if (red == -1 || green == -1 || blue == -1 || alpha > 1.0 || alpha < 0.0) {
RCTLogMustFix(@"Invalid color string '%@'", colorString);
} else {
color = [UIColor colorWithRed:red / 255.0 green:green / 255.0 blue:blue / 255.0 alpha:alpha];
}
} else if ([json isKindOfClass:[NSArray class]]) {
if ([json count] < 3 || [json count] > 4) {
RCTLogMustFix(@"Expected array with count 3 or 4, but count is %zd: %@", [json count], json);
} else {
// Color array
color = [UIColor colorWithRed:[json[0] doubleValue]
green:[json[1] doubleValue]
blue:[json[2] doubleValue]
alpha:[json count] > 3 ? [json[3] doubleValue] : 1];
}
} else if ([json isKindOfClass:[NSDictionary class]]) {
// Color dictionary
color = [UIColor colorWithRed:[json[@"r"] doubleValue]
green:[json[@"g"] doubleValue]
blue:[json[@"b"] doubleValue]
alpha:[json[@"a"] ?: @1 doubleValue]];
} else if (json && ![json isKindOfClass:[NSNull class]]) {
RCTLogMustFix(@"Expected NSArray, NSDictionary or NSString for UIColor, received %@: %@", [json class], json);
}
// Default color
if (!color) {
color = [UIColor whiteColor];
}
// Cache and return
if (json) {
colorCache[json] = color;
}
return color;
}
+ (CGColorRef)CGColor:(id)json
{
return [self UIColor:json].CGColor;
}
+ (UIImage *)UIImage:(id)json
{
if (![json isKindOfClass:[NSString class]]) {
RCTLogMustFix(@"Expected NSString for UIImage, received %@: %@", [json class], json);
return nil;
}
if ([json length] == 0) {
return nil;
}
UIImage *image = nil;
NSString *path = json;
if ([path isAbsolutePath]) {
image = [UIImage imageWithContentsOfFile:path];
} else {
image = [UIImage imageNamed:path];
if (!image) {
image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:path ofType:nil]];
}
}
if (!image) {
RCTLogWarn(@"No image was found at path %@", json);
}
return image;
}
+ (CGImageRef)CGImage:(id)json
{
return [self UIImage:json].CGImage;
}
+ (UIFont *)UIFont:(UIFont *)font withSize:(id)json
{
return [self UIFont:font withFamily:nil size:json weight:nil];
}
+ (UIFont *)UIFont:(UIFont *)font withWeight:(id)json
{
return [self UIFont:font withFamily:nil size:nil weight:json];
}
+ (UIFont *)UIFont:(UIFont *)font withFamily:(id)json
{
return [self UIFont:font withFamily:json size:nil weight:nil];
}
+ (UIFont *)UIFont:(UIFont *)font withFamily:(id)family size:(id)size weight:(id)weight
{
// Create descriptor
UIFontDescriptor *fontDescriptor = font.fontDescriptor ?: [UIFontDescriptor fontDescriptorWithName:RCTDefaultFontName size:RCTDefaultFontSize];
// Get font size
CGFloat fontSize = [self CGFloat:size];
if (fontSize && !isnan(fontSize)) {
fontDescriptor = [fontDescriptor fontDescriptorWithSize:fontSize];
}
// Get font family
NSString *familyName = [RCTConvert NSString:family];
if (familyName) {
if ([UIFont fontNamesForFamilyName:familyName].count == 0) {
UIFont *font = [UIFont fontWithName:familyName size:fontDescriptor.pointSize];
if (font) {
// It's actually a font name, not a font family name,
// but we'll do what was meant, not what was said.
familyName = font.familyName;
fontDescriptor = font.fontDescriptor;
} else {
// Not a valid font or family
RCTLogError(@"Unrecognized font family '%@'", familyName);
}
} else {
// Set font family
fontDescriptor = [fontDescriptor fontDescriptorWithFamily:familyName];
}
}
// Get font weight
NSString *fontWeight = [RCTConvert NSString:weight];
if (fontWeight) {
static NSSet *values;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
values = [NSSet setWithObjects:@"bold", @"normal", nil];
});
if (fontWeight && ![values containsObject:fontWeight]) {
RCTLogError(@"Unrecognized font weight '%@', must be one of %@", fontWeight, values);
}
UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits;
if ([fontWeight isEqualToString:RCTBoldFontWeight]) {
symbolicTraits |= UIFontDescriptorTraitBold;
} else {
symbolicTraits &= ~UIFontDescriptorTraitBold;
}
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
}
// TODO: font style
// Create font
return [UIFont fontWithDescriptor:fontDescriptor size:fontDescriptor.pointSize];
}
typedef BOOL css_overflow;
RCT_ENUM_CONVERTER(css_overflow, (@{
@"hidden": @NO,
@"visible": @YES
}), YES, boolValue)
RCT_ENUM_CONVERTER(css_flex_direction_t, (@{
@"row": @(CSS_FLEX_DIRECTION_ROW),
@"column": @(CSS_FLEX_DIRECTION_COLUMN)
}), CSS_FLEX_DIRECTION_COLUMN, intValue)
RCT_ENUM_CONVERTER(css_justify_t, (@{
@"flex-start": @(CSS_JUSTIFY_FLEX_START),
@"flex-end": @(CSS_JUSTIFY_FLEX_END),
@"center": @(CSS_JUSTIFY_CENTER),
@"space-between": @(CSS_JUSTIFY_SPACE_BETWEEN),
@"space-around": @(CSS_JUSTIFY_SPACE_AROUND)
}), CSS_JUSTIFY_FLEX_START, intValue)
RCT_ENUM_CONVERTER(css_align_t, (@{
@"flex-start": @(CSS_ALIGN_FLEX_START),
@"flex-end": @(CSS_ALIGN_FLEX_END),
@"center": @(CSS_ALIGN_CENTER),
@"auto": @(CSS_ALIGN_AUTO),
@"stretch": @(CSS_ALIGN_STRETCH)
}), CSS_ALIGN_FLEX_START, intValue)
RCT_ENUM_CONVERTER(css_position_type_t, (@{
@"absolute": @(CSS_POSITION_ABSOLUTE),
@"relative": @(CSS_POSITION_RELATIVE)
}), CSS_POSITION_RELATIVE, intValue)
RCT_ENUM_CONVERTER(css_wrap_type_t, (@{
@"wrap": @(CSS_WRAP),
@"nowrap": @(CSS_NOWRAP)
}), CSS_NOWRAP, intValue)
@end

View File

@@ -0,0 +1,52 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
#import "RCTModuleIDs.h"
/**
* Simple utility to help extract arguments to JS invocations of React event
* emitter (IOS version).
*/
@interface RCTEventExtractor : NSObject
+ (NSArray *)eventArgs:(NSNumber *)tag type:(RCTEventType)type nativeEventObj:(NSDictionary *)nativeEventObj;
/**
* Constructs information about touch events to send across the serialized
* boundary. This data should be compliant with W3C `Touch` objects. This data
* alone isn't sufficient to construct W3C `Event` objects. To construct that,
* there must be a simple receiver on the other side of the bridge that
* organizes the touch objects into `Event`s.
*
* We send the data as an array of `Touch`es, the type of action
* (start/end/move/cancel) and the indices that represent "changed" `Touch`es
* from that array.
*/
+ (NSArray *)touchEventArgsForOrderedTouches:(NSArray *)orderedTouches
orderedStartTags:(NSArray *)orderedStartTags
orderedTouchIDs:(NSArray *)orderedTouchIDs
changedIndices:(NSArray *)changedIndices
type:(RCTEventType)type
view:(UIView *)view;
+ (NSDictionary *)scrollEventObject:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag;
/**
* Useful when having to simply communicate the fact that *something* scrolled.
* When JavaScript infers gestures based on the event stream, any type of
* scroll that occurs in the native platform will cause ongoing gestures to
* cancel. Scroll/table views already send scroll events appropriately, but
* this method is useful for other views that don't actually scroll, but should
* interrupt JavaScript gestures as scrolls do.
*/
+ (NSDictionary *)fakeScrollEventObjectFor:(NSNumber *)reactTag;
/**
* Finds the React target of a touch. This must be done when the touch starts,
* else `UIKit` gesture recognizers may destroy the touch's target.
*/
+ (NSNumber *)touchStartTarget:(UITouch *)touch inView:(UIView *)view;
@end

View File

@@ -0,0 +1,200 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTEventExtractor.h"
#import "RCTLog.h"
#import "RCTUIManager.h"
#import "RCTViewNodeProtocol.h"
@implementation RCTEventExtractor
// TODO (#5906496): an array lookup would be better than a switch statement here
/**
* Keep in sync with `IOSEventConstants.js`.
* TODO (#5906496): do this sync programmatically instead of manually
*/
+ (NSString *)topLevelTypeForEventType:(RCTEventType)eventType
{
switch(eventType) {
case RCTEventTap:
return @"topTap";
case RCTEventVisibleCellsChange:
return @"topVisibleCellsChange";
case RCTEventNavigateBack:
return @"topNavigateBack";
case RCTEventNavRightButtonTap:
return @"topNavRightButtonTap";
case RCTEventChange:
return @"topChange";
case RCTEventTextFieldDidFocus:
return @"topFocus";
case RCTEventTextFieldWillBlur:
return @"topBlur";
case RCTEventTextFieldSubmitEditing:
return @"topSubmitEditing";
case RCTEventTextFieldEndEditing:
return @"topEndEditing";
case RCTEventTextInput:
return @"topTextInput";
case RCTEventLongPress:
return @"topLongPress"; // Not yet supported
case RCTEventTouchCancel:
return @"topTouchCancel";
case RCTEventTouchEnd:
return @"topTouchEnd";
case RCTEventTouchMove:
return @"topTouchMove";
case RCTEventTouchStart:
return @"topTouchStart";
case RCTEventScrollBeginDrag:
return @"topScrollBeginDrag";
case RCTEventScroll:
return @"topScroll";
case RCTEventScrollEndDrag:
return @"topScrollEndDrag";
case RCTEventScrollAnimationEnd:
return @"topScrollAnimationEnd";
case RCTEventSelectionChange:
return @"topSelectionChange";
case RCTEventMomentumScrollBegin:
return @"topMomentumScrollBegin";
case RCTEventMomentumScrollEnd:
return @"topMomentumScrollEnd";
case RCTEventPullToRefresh:
return @"topPullToRefresh";
case RCTEventLoadingStart:
return @"topLoadingStart";
case RCTEventLoadingFinish:
return @"topLoadingFinish";
case RCTEventLoadingError:
return @"topLoadingError";
case RCTEventNavigationProgress:
return @"topNavigationProgress";
default :
RCTLogError(@"Unrecognized event type: %tu", eventType);
return @"unknown";
}
}
/**
* TODO (#5906496): Cache created string messages for each event type/tag target (for
* events that have no data) to save allocations.
*/
+ (NSArray *)eventArgs:(NSNumber *)reactTag
type:(RCTEventType)type
nativeEventObj:(NSDictionary *)nativeEventObj
{
NSString *topLevelType = [RCTEventExtractor topLevelTypeForEventType:type];
return @[reactTag ?: @0, topLevelType, nativeEventObj];
}
+ (NSArray *)touchEventArgsForOrderedTouches:(NSArray *)orderedTouches
orderedStartTags:(NSArray *)orderedStartTags
orderedTouchIDs:(NSArray *)orderedTouchIDs
changedIndices:(NSArray *)changedIndices
type:(RCTEventType)type
view:(UIView *)view
{
if (!orderedTouches || !orderedTouches.count) {
RCTLogError(@"No touches in touchEventArgsForOrderedTouches");
return nil;
}
NSMutableArray *touchObjects = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < orderedTouches.count; i++) {
NSDictionary *touchObj =
[RCTEventExtractor touchObj:orderedTouches[i]
withTargetTag:orderedStartTags[i]
withTouchID:orderedTouchIDs[i]
inView:view];
[touchObjects addObject:touchObj];
}
NSString *topLevelType = [RCTEventExtractor topLevelTypeForEventType:type];
return @[topLevelType, touchObjects, changedIndices];
}
+ (NSNumber *)touchStartTarget:(UITouch *)touch inView:(UIView *)view
{
UIView <RCTViewNodeProtocol> *closestReactAncestor = [RCTUIManager closestReactAncestorThatRespondsToTouch:touch];
return [closestReactAncestor reactTag];
}
/**
* Constructs an object that contains all of the important touch data. This
* should contain a superset of a W3C `Touch` object. The `Event` objects that
* reference these touches can only be constructed from a collection of touch
* objects that are recieved across the serialized bridge.
*
* Must accept the `targetReactTag` because targets are reset to `nil` by
* `UIKit` gesture system. We had to pre-recorded at the time of touch start.
*/
+ (NSDictionary *)touchObj:(UITouch *)touch
withTargetTag:(NSNumber *)targetReactTag
withTouchID:(NSNumber *)touchID
inView:(UIView *)view
{
CGPoint location = [touch locationInView:view];
CGPoint locInView = [touch locationInView:touch.view];
double timeStamp = touch.timestamp * 1000.0; // convert to ms
return @{
@"pageX": @(location.x),
@"pageY": @(location.y),
@"locationX": @(locInView.x),
@"locationY": @(locInView.y),
@"target": targetReactTag,
@"identifier": touchID,
@"timeStamp": @(timeStamp),
@"touches": [NSNull null], // We hijack this touchObj to serve both as an event
@"changedTouches": [NSNull null], // and as a Touch object, so making this JIT friendly.
};
}
// TODO (#5906496): shouldn't some of these strings be constants?
+ (NSDictionary *)makeScrollEventObject:(UIScrollView *)uiScrollView reactTag:(NSNumber *)reactTag;
{
return @{
@"contentOffset": @{
@"x": @(uiScrollView.contentOffset.x),
@"y": @(uiScrollView.contentOffset.y)
},
@"contentSize": @{
@"width": @(uiScrollView.contentSize.width),
@"height": @(uiScrollView.contentSize.height)
},
@"layoutMeasurement": @{
@"width": @(uiScrollView.frame.size.width),
@"height": @(uiScrollView.frame.size.height)
},
@"zoomScale": @(uiScrollView.zoomScale),
@"target": reactTag,
};
}
+ (NSDictionary *)scrollEventObject:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag
{
return [RCTEventExtractor makeScrollEventObject:scrollView reactTag:reactTag];
}
+ (NSDictionary *)fakeScrollEventObjectFor:(NSNumber *)reactTag
{
return @{
@"contentOffset": @{
@"x": @0,
@"y": @0
},
@"contentSize": @{
@"width": @0,
@"height": @0
},
@"layoutMeasurement": @{
@"width": @0,
@"height": @0
},
@"zoomScale": @1,
@"target": reactTag
};
}
@end

175
ReactKit/Base/RCTExport.h Normal file
View File

@@ -0,0 +1,175 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
#import "RCTLog.h"
@class RCTSparseArray;
@class RCTUIManager;
typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *viewRegistry);
@class RCTJavaScriptEventDispatcher;
@class RCTShadowView;
/* ------------------------------------------------------------------- */
typedef struct {
const char *func;
const char *js_name;
} RCTExportEntry;
#define _RCTExportSegmentName "__DATA"
#define _RCTExportSectionName "RCTExport"
extern NSString *RCTExportedModuleNameAtSortedIndex(NSUInteger index);
extern NSDictionary *RCTExportedMethodsByModule(void);
extern BOOL RCTSetProperty(id target, NSString *keypath, id value);
extern BOOL RCTCallSetter(id target, SEL setter, id value);
/* ------------------------------------------------------------------- */
/**
* The type of a block that is capable of sending a response to a bridged
* operation. Use this for returning callback methods to JS.
*/
typedef void (^RCTResponseSenderBlock)(NSArray *response);
/**
* Provides minimal interface needed to register a bridge module
*/
@protocol RCTNativeModule <NSObject>
@optional
/**
* Place this macro inside the method body of any method you want
* to expose to JS. The optional js_name argument will be used as
* the JS function name. If omitted, the JS function name will match
* the Objective-C method selector name, up to the first colon.
*/
#define RCT_EXPORT(js_name) __attribute__((used, section(_RCTExportSegmentName "," \
_RCTExportSectionName))) static const RCTExportEntry __rct_export_entry__ = { __func__, #js_name }
/**
* The module name exposed to JS. If omitted, this will be inferred
* automatically by using the native module's class name.
*/
+ (NSString *)moduleName;
/**
* Injects constants into JS. These constants are made accessible via
* NativeModules.moduleName.X.
*/
- (NSDictionary *)constantsToExport;
/**
* Notifies the module that a batch of JS method invocations has just completed.
*/
- (void)batchDidComplete;
@end
/**
* Provides minimal interface needed to register a UIViewManager module
*/
@protocol RCTNativeViewModule <RCTNativeModule>
/**
* This method instantiates a native view to be managed by the module.
*/
- (UIView *)viewWithEventDispatcher:(RCTJavaScriptEventDispatcher *)eventDispatcher;
@optional
/**
* This method instantiates a shadow view to be managed by the module. If omitted,
* an ordinary RCTShadowView instance will be created.
*/
- (RCTShadowView *)shadowView;
/**
* Informal protocol for setting view and shadowView properties.
* Implement methods matching these patterns to set any properties that
* require special treatment (e.g. where the type or name cannot be inferred).
*
* - (void)set_<propertyName>:(id)property
* forView:(UIView *)view
* withDefaultView:(UIView *)defaultView;
*
* - (void)set_<propertyName>:(id)property
* forShadowView:(RCTShadowView *)view
* withDefaultView:(RCTShadowView *)defaultView;
*
* For simple cases, use the macros below:
*/
/**
* This handles the simple case, where JS and native property names match
* And the type can be automatically inferred.
*/
#define RCT_EXPORT_VIEW_PROPERTY(name) \
RCT_REMAP_VIEW_PROPERTY(name, name)
/**
* This macro maps a named property on the module to an arbitrary key path
* within the view.
*/
#define RCT_REMAP_VIEW_PROPERTY(name, keypath) \
- (void)set_##name:(id)json forView:(id)view withDefaultView:(id)defaultView { \
if (json) { \
if(!RCTSetProperty(view, @#keypath, json)) { \
RCTLogMustFix(@"%@ does not have setter for `%s` property", [view class], #name); \
} \
} else { \
[view setValue:[defaultView valueForKeyPath:@#keypath] forKeyPath:@#keypath]; \
} \
}
/**
* These are useful in cases where the module's superclass handles a
* property, but you wish to "unhandle" it, so it will be ignored.
*/
#define RCT_IGNORE_VIEW_PROPERTY(name) \
- (void)set_##name:(id)value forView:(id)view withDefaultView:(id)defaultView {}
#define RCT_IGNORE_SHADOW_PROPERTY(name) \
- (void)set_##name:(id)value forShadowView:(id)view withDefaultView:(id)defaultView {}
/**
* Returns a dictionary of config data passed to JS that defines eligible events
* that can be placed on native views. This should return bubbling
* directly-dispatched event types and specify what names should be used to
* subscribe to either form (bubbling/capturing).
*
* Returned dictionary should be of the form: @{
* @"onTwirl": {
* @"phasedRegistrationNames": @{
* @"bubbled": @"onTwirl",
* @"captured": @"onTwirlCaptured"
* }
* }
* }
*/
- (NSDictionary *)customBubblingEventTypes;
/**
* Returns a dictionary of config data passed to JS that defines eligible events
* that can be placed on native views. This should return non-bubbling
* directly-dispatched event types.
*
* Returned dictionary should be of the form: @{
* @"onTwirl": {
* @"registrationName": @"onTwirl"
* }
* }
*/
- (NSDictionary *)customDirectEventTypes;
/**
* To deprecate, hopefully
*/
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry;
@end

362
ReactKit/Base/RCTExport.m Normal file
View File

@@ -0,0 +1,362 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTExport.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
#import <mach-o/getsect.h>
#import <mach-o/dyld.h>
#import <objc/runtime.h>
#import <objc/message.h>
#import "RCTConvert.h"
#import "RCTModuleMethod.h"
#import "RCTUtils.h"
static NSDictionary *_methodsByModule;
@interface _RCTExportLoader : NSObject
@end
@implementation _RCTExportLoader
+ (NSString *)methodNameForSelector:(SEL)selector
{
NSString *methodName = NSStringFromSelector(selector);
NSRange colonRange = [methodName rangeOfString:@":"];
if (colonRange.location != NSNotFound) {
methodName = [methodName substringToIndex:colonRange.location];
}
return methodName;
}
+ (NSIndexSet *)blockArgumentIndexesForMethod:(Method)method
{
unsigned int argumentCount = method_getNumberOfArguments(method);
NSMutableIndexSet *blockArgumentIndexes = [NSMutableIndexSet indexSet];
static const char *blockType = @encode(typeof(^{}));
for (unsigned int i = 2; i < argumentCount; i++) {
char *type = method_copyArgumentType(method, i);
if (!strcmp(type, blockType)) {
[blockArgumentIndexes addIndex:i - 2];
}
free(type);
}
return [blockArgumentIndexes copy];
}
+ (void)load
{
static uint32_t _exportsLoaded = 0;
if (OSAtomicTestAndSetBarrier(1, &_exportsLoaded)) {
return;
}
#ifdef __LP64__
typedef uint64_t RCTExportValue;
typedef struct section_64 RCTExportSection;
#define RCTGetSectByNameFromHeader getsectbynamefromheader_64
#else
typedef uint32_t RCTExportValue;
typedef struct section RCTExportSection;
#define RCTGetSectByNameFromHeader getsectbynamefromheader
#endif
Dl_info info;
dladdr(&RCTExportedMethodsByModule, &info);
const RCTExportValue mach_header = (RCTExportValue)info.dli_fbase;
const RCTExportSection *section = RCTGetSectByNameFromHeader((void *)mach_header, _RCTExportSegmentName, _RCTExportSectionName);
if (section == NULL) {
return;
}
NSMutableDictionary *methodsByModule = [NSMutableDictionary dictionary];
NSCharacterSet *plusMinusCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"+-"];
for (RCTExportValue addr = section->offset;
addr < section->offset + section->size;
addr += sizeof(RCTExportEntry)) {
RCTExportEntry *entry = (RCTExportEntry *)(mach_header + addr);
NSScanner *scanner = [NSScanner scannerWithString:@(entry->func)];
NSString *plusMinus;
if (![scanner scanCharactersFromSet:plusMinusCharacterSet intoString:&plusMinus]) continue;
if (![scanner scanString:@"[" intoString:NULL]) continue;
NSString *className;
if (![scanner scanUpToString:@" " intoString:&className]) continue;
[scanner scanString:@" " intoString:NULL];
NSString *selectorName;
if (![scanner scanUpToString:@"]" intoString:&selectorName]) continue;
Class class = NSClassFromString(className);
if (class == Nil) continue;
SEL selector = NSSelectorFromString(selectorName);
Method method = ([plusMinus characterAtIndex:0] == '+' ? class_getClassMethod : class_getInstanceMethod)(class, selector);
if (method == nil) continue;
NSString *JSMethodName = strlen(entry->js_name) ? @(entry->js_name) : [self methodNameForSelector:selector];
RCTModuleMethod *moduleMethod =
[[RCTModuleMethod alloc] initWithSelector:selector
JSMethodName:JSMethodName
arity:method_getNumberOfArguments(method) - 2
blockArgumentIndexes:[self blockArgumentIndexesForMethod:method]];
// TODO: store these by class name, not module name, then we don't need to call moduleName here
NSString *moduleName = [class respondsToSelector:@selector(moduleName)] ? [class moduleName] : className;
NSArray *moduleMap = methodsByModule[moduleName];
methodsByModule[moduleName] = (moduleMap != nil) ? [moduleMap arrayByAddingObject:moduleMethod] : @[moduleMethod];
}
_methodsByModule = [methodsByModule copy];
}
@end
NSDictionary *RCTExportedMethodsByModule(void)
{
return _methodsByModule;
}
NSString *RCTExportedModuleNameAtSortedIndex(NSUInteger index)
{
static dispatch_once_t onceToken;
static NSArray *sortedModuleNames;
dispatch_once(&onceToken, ^{
sortedModuleNames = [RCTExportedMethodsByModule().allKeys sortedArrayUsingSelector:@selector(compare:)];
});
return sortedModuleNames[index];
}
static NSString *RCTGuessTypeEncoding(id target, NSString *key, id value, NSString *encoding)
{
// TODO (#5906496): handle more cases
if ([key rangeOfString:@"color" options:NSCaseInsensitiveSearch].location != NSNotFound) {
if ([target isKindOfClass:[CALayer class]]) {
return @(@encode(CGColorRef));
} else {
return @"@\"UIColor\"";
}
}
return nil;
}
static NSDictionary *RCTConvertValue(id value, NSString *encoding)
{
static NSDictionary *converters = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
id (^numberConvert)(id) = ^(id val){
return [RCTConvert NSNumber:val];
};
id (^boolConvert)(id) = ^(id val){
return @([RCTConvert BOOL:val]);
};
// TODO (#5906496): add the rest of RCTConvert here
converters =
@{
@(@encode(char)): boolConvert,
@(@encode(int)): numberConvert,
@(@encode(short)): numberConvert,
@(@encode(long)): numberConvert,
@(@encode(long long)): numberConvert,
@(@encode(unsigned char)): numberConvert,
@(@encode(unsigned int)): numberConvert,
@(@encode(unsigned short)): numberConvert,
@(@encode(unsigned long)): numberConvert,
@(@encode(unsigned long long)): numberConvert,
@(@encode(float)): numberConvert,
@(@encode(double)): numberConvert,
@(@encode(bool)): boolConvert,
@(@encode(UIEdgeInsets)): ^(id val) {
return [NSValue valueWithUIEdgeInsets:[RCTConvert UIEdgeInsets:val]];
},
@(@encode(CGPoint)): ^(id val) {
return [NSValue valueWithCGPoint:[RCTConvert CGPoint:val]];
},
@(@encode(CGSize)): ^(id val) {
return [NSValue valueWithCGSize:[RCTConvert CGSize:val]];
},
@(@encode(CGRect)): ^(id val) {
return [NSValue valueWithCGRect:[RCTConvert CGRect:val]];
},
@(@encode(CGColorRef)): ^(id val) {
return (id)[RCTConvert CGColor:val];
},
@(@encode(CGAffineTransform)): ^(id val) {
return [NSValue valueWithCGAffineTransform:[RCTConvert CGAffineTransform:val]];
},
@(@encode(CATransform3D)): ^(id val) {
return [NSValue valueWithCATransform3D:[RCTConvert CATransform3D:val]];
},
@"@\"NSString\"": ^(id val) {
return [RCTConvert NSString:val];
},
@"@\"NSURL\"": ^(id val) {
return [RCTConvert NSURL:val];
},
@"@\"UIColor\"": ^(id val) {
return [RCTConvert UIColor:val];
},
@"@\"UIImage\"": ^(id val) {
return [RCTConvert UIImage:val];
},
@"@\"NSDate\"": ^(id val) {
return [RCTConvert NSDate:val];
},
@"@\"NSTimeZone\"": ^(id val) {
return [RCTConvert NSTimeZone:val];
},
};
});
// Handle null values
if (value == [NSNull null] && ![encoding isEqualToString:@"@\"NSNull\""]) {
return nil;
}
// Convert value
id (^converter)(id) = converters[encoding];
return converter ? converter(value) : value;
}
BOOL RCTSetProperty(id target, NSString *keypath, id value)
{
// Split keypath
NSArray *parts = [keypath componentsSeparatedByString:@"."];
NSString *key = [parts lastObject];
for (NSUInteger i = 0; i < parts.count - 1; i++) {
target = [target valueForKey:parts[i]];
if (!target) {
return NO;
}
}
// Check target class for property definition
NSString *encoding = nil;
objc_property_t property = class_getProperty([target class], [key UTF8String]);
if (property) {
// Get type info
char *typeEncoding = property_copyAttributeValue(property, "T");
encoding = @(typeEncoding);
free(typeEncoding);
} else {
// Check if setter exists
SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:",
[[key substringToIndex:1] uppercaseString],
[key substringFromIndex:1]]);
if (![target respondsToSelector:setter]) {
return NO;
}
// Get type of first method argument
Method method = class_getInstanceMethod([target class], setter);
char *typeEncoding = method_copyArgumentType(method, 2);
if (typeEncoding) {
encoding = @(typeEncoding);
free(typeEncoding);
}
}
if (encoding.length == 0 || [encoding isEqualToString:@(@encode(id))]) {
// Not enough info about the type encoding to be useful, so
// try to guess the type from the value and property name
encoding = RCTGuessTypeEncoding(target, key, value, encoding);
}
// Special case for numeric encodings, which may be enums
if ([value isKindOfClass:[NSString class]] &&
[@"iIsSlLqQ" containsString:[encoding substringToIndex:1]]) {
/**
* NOTE: the property names below may seem weird, but it's
* because they are tested as case-sensitive suffixes, so
* "apitalizationType" will match any of the following
*
* - capitalizationType
* - autocapitalizationType
* - autoCapitalizationType
* - titleCapitalizationType
* - etc.
*/
static NSDictionary *converters = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
converters =
@{
@"apitalizationType": ^(id val) {
return [RCTConvert UITextAutocapitalizationType:val];
},
@"eyboardType": ^(id val) {
return [RCTConvert UIKeyboardType:val];
},
@"extAlignment": ^(id val) {
return [RCTConvert NSTextAlignment:val];
},
};
});
for (NSString *subkey in converters) {
if ([key hasSuffix:subkey]) {
NSInteger (^converter)(NSString *) = converters[subkey];
value = @(converter(value));
break;
}
}
}
// Another nasty special case
if ([target isKindOfClass:[UITextField class]]) {
static NSDictionary *specialCases = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
specialCases = @{
@"autocapitalizationType": ^(UITextField *f, NSInteger v){ f.autocapitalizationType = v; },
@"autocorrectionType": ^(UITextField *f, NSInteger v){ f.autocorrectionType = v; },
@"spellCheckingType": ^(UITextField *f, NSInteger v){ f.spellCheckingType = v; },
@"keyboardType": ^(UITextField *f, NSInteger v){ f.keyboardType = v; },
@"keyboardAppearance": ^(UITextField *f, NSInteger v){ f.keyboardAppearance = v; },
@"returnKeyType": ^(UITextField *f, NSInteger v){ f.returnKeyType = v; },
@"enablesReturnKeyAutomatically": ^(UITextField *f, NSInteger v){ f.enablesReturnKeyAutomatically = !!v; },
@"secureTextEntry": ^(UITextField *f, NSInteger v){ f.secureTextEntry = !!v; }};
});
void (^block)(UITextField *f, NSInteger v) = specialCases[key];
if (block)
{
block(target, [value integerValue]);
return YES;
}
}
// Set converted value
[target setValue:RCTConvertValue(value, encoding) forKey:key];
return YES;
}
BOOL RCTCallSetter(id target, SEL setter, id value)
{
// Get property name
NSString *propertyName = NSStringFromSelector(setter);
RCTCAssert([propertyName hasPrefix:@"set"] && [propertyName hasSuffix:@":"],
@"%@ is not a valid setter name", propertyName);
propertyName = [[[propertyName substringWithRange:(NSRange){3,1}] lowercaseString] stringByAppendingString:[propertyName substringWithRange:(NSRange){4,propertyName.length - 5}]];
// Set property
return RCTSetProperty(target, propertyName, value);
}

View File

@@ -0,0 +1,18 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error);
@interface RCTImageDownloader : NSObject
+ (instancetype)sharedInstance;
- (id)downloadImageForURL:(NSURL *)url
size:(CGSize)size
scale:(CGFloat)scale
block:(RCTImageDownloadBlock)block;
- (void)cancelDownload:(id)downloadToken;
@end

View File

@@ -0,0 +1,67 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTImageDownloader.h"
#import "RCTUtils.h"
// TODO: something a bit more sophisticated
@implementation RCTImageDownloader
+ (instancetype)sharedInstance
{
RCTAssertMainThread();
static RCTImageDownloader *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (id)downloadImageForURL:(NSURL *)url
size:(CGSize)size
scale:(CGFloat)scale
block:(RCTImageDownloadBlock)block
{
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
UIImage *image = [UIImage imageWithData:data scale:scale];
// TODO: cache compressed image
CGSize imageSize = size;
if (CGSizeEqualToSize(imageSize, CGSizeZero)) {
imageSize = image.size;
}
CGFloat imageScale = scale;
if (imageScale == 0 || imageScale > image.scale) {
imageScale = image.scale;
}
if (image) {
// Decompress on background thread
UIGraphicsBeginImageContextWithOptions(imageSize, NO, imageScale);
[image drawInRect:(CGRect){{0, 0}, imageSize}];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// TODO: cache decompressed images at each requested size
}
// Dispatch back to main thread
dispatch_async(dispatch_get_main_queue(), ^{
block(image, error);
});
}];
[task resume];
return task;
}
- (void)cancelDownload:(id)downloadToken
{
[(NSURLSessionDataTask *)downloadToken cancel];
}
@end

View File

@@ -0,0 +1,16 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <Foundation/Foundation.h>
// TODO (#5906496): is there a reason for this protocol? It seems to be
// used in a number of places where it isn't really required - only the
// RCTBridge actually ever calls casts to it - in all other
// cases it is simply a way of adding some method definitions to classes
@protocol RCTInvalidating <NSObject>
@property (nonatomic, assign, readonly, getter = isValid) BOOL valid;
- (void)invalidate;
@end

View File

@@ -0,0 +1,25 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
#import "RCTInvalidating.h"
#import "RCTJavaScriptExecutor.h"
@class RCTBridge;
/**
* Class that allows easy embedding, loading, life-cycle management of a
* JavaScript application inside of a native application.
* TODO: Before loading new application source, publish global notification in
* JavaScript so that applications can clean up resources. (launch blocker).
* TODO: Incremental module loading. (low pri).
*/
@interface RCTJavaScriptAppEngine : NSObject <RCTInvalidating>
@property (nonatomic, readonly, strong) RCTBridge *bridge;
- (instancetype)initWithBridge:(RCTBridge *)bridge;
- (void)loadBundleAtURL:(NSURL *)moduleURL useCache:(BOOL)useCache onComplete:(RCTJavaScriptCompleteBlock)onComplete;
+ (void)resetCacheForBundleAtURL:(NSURL *)moduleURL;
@end

View File

@@ -0,0 +1,266 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTJavaScriptAppEngine.h"
#import "RCTBridge.h"
#import "RCTInvalidating.h"
#import "RCTLog.h"
#import "RCTRedBox.h"
#import "RCTUtils.h"
#define JS_SERVER_NOT_AVAILABLE @"Could not connect to development server. Ensure node server is running - run 'npm start' from ReactKit root"
#define CACHE_DIR @"RCTJSBundleCache"
#pragma mark - Application Engine
/**
* TODO:
* - Add window resize rotation events matching the DOM API.
* - Device pixel ration hooks.
* - Source maps.
*/
@implementation RCTJavaScriptAppEngine
{
BOOL _isPaused; // Pauses drawing/updating of the JSView
BOOL _pauseOnEnterBackground;
CADisplayLink *_displayLink;
NSTimer *_runTimer;
NSDictionary *_loadedResource;
}
- (instancetype)init
{
RCT_NOT_DESIGNATED_INITIALIZER();
}
/**
* `CADisplayLink` code copied from Ejecta but we've placed the JavaScriptCore
* engine in its own dedicated thread.
*
* TODO: Try adding to the `RCTJavaScriptExecutor`'s thread runloop. Removes one
* additional GCD dispatch per frame and likely makes it so that other UIThread
* operations don't delay the dispatch (so we can begin working in JS much
* faster.) Event handling must still be sent via a GCD dispatch, of course.
*
* We must add the display link to two runloops in order to get setTimeouts to
* fire during scrolling. (`NSDefaultRunLoopMode` and `UITrackingRunLoopMode`)
* TODO: We can invent a `requestAnimationFrame` and
* `requestAvailableAnimationFrame` to control if callbacks can be fired during
* an animation.
* http://stackoverflow.com/questions/12622800/why-does-uiscrollview-pause-my-cadisplaylink
*
*/
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
RCTAssertMainThread();
if ((self = [super init])) {
_bridge = bridge;
_isPaused = NO;
self.pauseOnEnterBackground = YES;
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(run:)];
if (_displayLink) {
[_displayLink setFrameInterval:1];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
} else {
RCTLogWarn(@"Failed to create a display link (probably on buildbot) - using an NSTimer for AppEngine instead.");
_runTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) target:self selector:@selector(run:) userInfo:nil repeats:YES];
}
}
return self;
}
/**
* TODO: Wait until operations on `javaScriptQueue` are complete.
*/
- (void)dealloc
{
RCTAssert(!self.valid, @"-invalidate must be called before -dealloc");
}
#pragma mark - RCTInvalidating
- (BOOL)isValid
{
return _displayLink != nil;
}
- (void)invalidate
{
[_bridge invalidate];
_bridge = nil;
[_displayLink invalidate];
_displayLink = nil;
// Remove from notification center
self.pauseOnEnterBackground = NO;
}
#pragma mark - Run loop
- (void)run:(CADisplayLink *)sender
{
if (!_isPaused) {
RCTAssertMainThread();
[_bridge enqueueUpdateTimers];
}
}
- (void)pauseRunLoop
{
if (!_isPaused) {
[_displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
_isPaused = YES;
}
}
- (void)resumeRunLoop
{
if (_isPaused) {
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
_isPaused = NO;
}
}
/**
* See warnings from lint: UIApplicationDidBecomeActive fires in a critical
* foreground path, and prevents the app from prioritizing the foreground
* processing. Consider using
* FBApplicationDidFinishEnteringForegroundAndIsNowIdleNotification.
*/
- (void)setPauseOnEnterBackground:(BOOL)pauses
{
NSArray *pauseN = @[
UIApplicationWillResignActiveNotification,
UIApplicationDidEnterBackgroundNotification,
UIApplicationWillTerminateNotification
];
NSArray *resumeN =
@[UIApplicationWillEnterForegroundNotification, UIApplicationDidBecomeActiveNotification];
if (pauses) {
[self observeKeyPaths:pauseN selector:@selector(pauseRunLoop)];
[self observeKeyPaths:resumeN selector:@selector(resumeRunLoop)];
}
else {
[self removeObserverForKeyPaths:pauseN];
[self removeObserverForKeyPaths:resumeN];
}
_pauseOnEnterBackground = pauses;
}
- (void)removeObserverForKeyPaths:(NSArray*)keyPaths
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
for (NSString *name in keyPaths) {
[nc removeObserver:self name:name object:nil];
}
}
- (void)observeKeyPaths:(NSArray*)keyPaths selector:(SEL)selector
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
for (NSString *name in keyPaths) {
[nc addObserver:self selector:selector name:name object:nil];
}
}
#pragma mark - Module and script loading
+ (void)resetCacheForBundleAtURL:(NSURL *)moduleURL
{
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *fileDir = [rootPath stringByAppendingPathComponent:CACHE_DIR];
NSString *filePath = [fileDir stringByAppendingPathComponent:RCTMD5Hash(moduleURL.absoluteString)];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:NULL];
}
/**
* TODO: All loading of script via network or disk should be done in a separate
* thread, not the JS thread, and not the main UI thread (launch blocker).
*/
- (void)loadBundleAtURL:(NSURL *)moduleURL useCache:(BOOL)useCache onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
NSString *cachedFilePath;
if (useCache) {
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *fileDir = [rootPath stringByAppendingPathComponent:CACHE_DIR];
cachedFilePath = [fileDir stringByAppendingPathComponent:RCTMD5Hash(moduleURL.absoluteString)];
if ([[NSFileManager defaultManager] fileExistsAtPath:cachedFilePath]) {
NSError *error;
NSString *rawText = [NSString stringWithContentsOfFile:cachedFilePath encoding:NSUTF8StringEncoding error:&error];
if (rawText.length == 0 || error != nil) {
if (onComplete) onComplete(error);
} else {
[self _enqueueLoadBundleResource:rawText url:moduleURL onComplete:onComplete];
}
return;
}
}
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:moduleURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error != nil) {
if ([[error domain] isEqualToString:NSURLErrorDomain]) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: JS_SERVER_NOT_AVAILABLE,
NSLocalizedFailureReasonErrorKey: [error localizedDescription],
NSUnderlyingErrorKey: error,
};
error = [NSError errorWithDomain:@"JSServer"
code:error.code
userInfo:userInfo];
}
if (onComplete) onComplete(error);
return;
}
NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName != nil) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
if (cfEncoding != kCFStringEncodingInvalidId) {
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
}
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
NSDictionary *userInfo;
NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
if ([errorDetails isKindOfClass:[NSDictionary class]]) {
userInfo = @{
NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
@"stack": @[@{
@"methodName": errorDetails[@"description"] ?: @"",
@"file": errorDetails[@"filename"] ?: @"",
@"lineNumber": errorDetails[@"lineNumber"] ?: @0
}]
};
} else {
userInfo = @{NSLocalizedDescriptionKey: rawText};
}
NSError *serverError = [NSError errorWithDomain:@"JSServer"
code:[(NSHTTPURLResponse *)response statusCode]
userInfo:userInfo];
if (onComplete) onComplete(serverError);
return;
}
if (useCache) {
[[NSFileManager defaultManager] createDirectoryAtPath:cachedFilePath.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:NULL];
[rawText writeToFile:cachedFilePath atomically:YES encoding:encoding error:NULL];
}
[self _enqueueLoadBundleResource:rawText url:moduleURL onComplete:onComplete];
}];
[task resume];
}
- (void)_enqueueLoadBundleResource:(NSString *)rawText url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
[_bridge enqueueApplicationScript:rawText url:url onComplete:onComplete];
}
@end

View File

@@ -0,0 +1,14 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <Foundation/Foundation.h>
@class RCTBridge;
@interface RCTJavaScriptEventDispatcher : NSObject
- (instancetype)initWithBridge:(RCTBridge *)bridge;
- (void)sendDeviceEventWithArgs:(NSArray *)args;
- (void)sendEventWithArgs:(NSArray *)args;
- (void)sendTouchesWithArgs:(NSArray *)args;
@end

View File

@@ -0,0 +1,51 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTJavaScriptEventDispatcher.h"
#import "RCTBridge.h"
#import "RCTModuleIDs.h"
@implementation RCTJavaScriptEventDispatcher
{
RCTBridge *_bridge;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
}
return self;
}
- (void)sendDeviceEventWithArgs:(NSArray *)args
{
if (!args) {
return;
}
[_bridge enqueueJSCall:RCTModuleIDDeviceEventEmitter
methodID:RCTDeviceEventEmitterEmit
args:args];
}
- (void)sendEventWithArgs:(NSArray *)args
{
if (!args) {
return;
}
[_bridge enqueueJSCall:RCTModuleIDReactIOSEventEmitter
methodID:RCTEventEmitterReceiveEvent
args:args];
}
- (void)sendTouchesWithArgs:(NSArray *)args
{
if (!args) {
return;
}
[_bridge enqueueJSCall:RCTModuleIDReactIOSEventEmitter
methodID:RCTEventEmitterReceiveTouches
args:args];
}
@end

View File

@@ -0,0 +1,35 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <JavaScriptCore/JavaScriptCore.h>
#import "RCTInvalidating.h"
typedef void (^RCTJavaScriptCompleteBlock)(NSError *error);
typedef void (^RCTJavaScriptCallback)(id objcValue, NSError *error);
/**
* Abstracts away a JavaScript execution context - we may be running code in a
* web view (for debugging purposes), or may be running code in a `JSContext`.
*/
@protocol RCTJavaScriptExecutor <RCTInvalidating>
/**
* Executes given method with arguments on JS thread and calls the given callback
* with JSValue and JSContext as a result of the JS module call.
*/
- (void)executeJSCall:(NSString *)name
method:(NSString *)method
arguments:(NSArray *)arguments
callback:(RCTJavaScriptCallback)onComplete;
/**
* Runs an application script, and notifies of the script load being complete via `onComplete`.
*/
- (void)executeApplicationScript:(NSString *)script
sourceURL:(NSURL *)url
onComplete:(RCTJavaScriptCompleteBlock)onComplete;
- (void)injectJSONText:(NSString *)script
asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete;
@end

View File

@@ -0,0 +1,28 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
@interface RCTKeyCommands : NSObject
+ (instancetype)sharedInstance;
/**
* Register a keyboard command.
*/
- (void)registerKeyCommandWithInput:(NSString *)input
modifierFlags:(UIKeyModifierFlags)flags
action:(void (^)(UIKeyCommand *command))block;
/**
* Unregister a keyboard command.
*/
- (void)unregisterKeyCommandWithInput:(NSString *)input
modifierFlags:(UIKeyModifierFlags)flags;
/**
* Check if a command is registered.
*/
- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input
modifierFlags:(UIKeyModifierFlags)flags;
@end

View File

@@ -0,0 +1,116 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTKeyCommands.h"
#import <UIKit/UIKit.h>
#import "RCTUtils.h"
@interface RCTKeyCommands ()
@property (nonatomic, strong) NSMutableDictionary *commandBindings;
- (void)RCT_handleKeyCommand:(UIKeyCommand *)key;
@end
@implementation UIApplication (RCTKeyCommands)
- (NSArray *)RCT_keyCommands
{
NSDictionary *commandBindings = [RCTKeyCommands sharedInstance].commandBindings;
return [[self RCT_keyCommands] arrayByAddingObjectsFromArray:[commandBindings allKeys]];
}
- (BOOL)RCT_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event
{
if (action == @selector(RCT_handleKeyCommand:)) {
[[RCTKeyCommands sharedInstance] RCT_handleKeyCommand:sender];
return YES;
}
return [self RCT_sendAction:action to:target from:sender forEvent:event];
}
@end
@implementation RCTKeyCommands
+ (void)initialize
{
//swizzle UIApplication
RCTSwapInstanceMethods([UIApplication class], @selector(keyCommands), @selector(RCT_keyCommands));
RCTSwapInstanceMethods([UIApplication class], @selector(sendAction:to:from:forEvent:), @selector(RCT_sendAction:to:from:forEvent:));
}
static RCTKeyCommands *RKKeyCommandsSharedInstance = nil;
+ (instancetype)sharedInstance
{
static RCTKeyCommands *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (instancetype)init
{
if ((self = [super init])) {
_commandBindings = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)RCT_handleKeyCommand:(UIKeyCommand *)key
{
// NOTE: We should just be able to do commandBindings[key] here, but curiously, the
// lookup seems to return nil sometimes, even if the key is found in the dictionary.
// To fix this, we use a linear search, since there won't be many keys anyway
[_commandBindings enumerateKeysAndObjectsUsingBlock:^(UIKeyCommand *k, void (^block)(UIKeyCommand *), BOOL *stop) {
if ([key.input isEqualToString:k.input] && key.modifierFlags == k.modifierFlags) {
block(key);
}
}];
}
- (void)registerKeyCommandWithInput:(NSString *)input
modifierFlags:(UIKeyModifierFlags)flags
action:(void (^)(UIKeyCommand *))block
{
RCTAssertMainThread();
UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input
modifierFlags:flags
action:@selector(RCT_handleKeyCommand:)];
_commandBindings[command] = block;
}
- (void)unregisterKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags
{
RCTAssertMainThread();
for (UIKeyCommand *key in [_commandBindings allKeys]) {
if ([key.input isEqualToString:input] && key.modifierFlags == flags) {
[_commandBindings removeObjectForKey:key];
break;
}
}
}
- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input
modifierFlags:(UIKeyModifierFlags)flags
{
RCTAssertMainThread();
for (UIKeyCommand *key in [_commandBindings allKeys]) {
if ([key.input isEqualToString:input] && key.modifierFlags == flags) {
return YES;
}
}
return NO;
}
@end

127
ReactKit/Base/RCTLog.h Normal file
View File

@@ -0,0 +1,127 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTAssert.h"
#import "RCTRedBox.h"
#define RCTLOG_INFO 1
#define RCTLOG_WARN 2
#define RCTLOG_ERROR 3
#define RCTLOG_MUSTFIX 4
// If set to e.g. `RCTLOG_ERROR`, will assert after logging the first error.
#if DEBUG
#define RCTLOG_FATAL_LEVEL RCTLOG_MUSTFIX
#define RCTLOG_REDBOX_LEVEL RCTLOG_ERROR
#else
#define RCTLOG_FATAL_LEVEL (RCTLOG_MUSTFIX + 1)
#define RCTLOG_REDBOX_LEVEL (RCTLOG_MUSTFIX + 1)
#endif
// If defined, only log messages that match this regex will fatal
#define RCTLOG_FATAL_REGEX nil
#define _RCTLog(__RCTLog__level, ...) do { \
NSString *__RCTLog__levelStr; \
switch(__RCTLog__level) { \
case RCTLOG_INFO: __RCTLog__levelStr = @"info"; break; \
case RCTLOG_WARN: __RCTLog__levelStr = @"warn"; break; \
case RCTLOG_ERROR: __RCTLog__levelStr = @"error"; break; \
case RCTLOG_MUSTFIX: __RCTLog__levelStr = @"mustfix"; break; \
} \
NSString *__RCTLog__msg = _RCTLogObjects(RCTLogFormat(__VA_ARGS__), __RCTLog__levelStr); \
if (__RCTLog__level >= RCTLOG_FATAL_LEVEL) { \
BOOL __RCTLog__fail = YES; \
if (RCTLOG_FATAL_REGEX) { \
NSError *__RCTLog__e; \
NSRegularExpression *__RCTLog__regex = [NSRegularExpression regularExpressionWithPattern:RCTLOG_FATAL_REGEX options:0 error:&__RCTLog__e]; \
__RCTLog__fail = [__RCTLog__regex numberOfMatchesInString:__RCTLog__msg options:0 range:NSMakeRange(0, [__RCTLog__msg length])] > 0; \
} \
RCTCAssert(!__RCTLog__fail, @"RCTLOG_FATAL_LEVEL %@: %@", __RCTLog__levelStr, __RCTLog__msg); \
} \
if (__RCTLog__level >= RCTLOG_REDBOX_LEVEL) { \
RCTRedBox *__RCTLog__redBox = [RCTRedBox sharedInstance]; \
[__RCTLog__redBox showErrorMessage:__RCTLog__msg]; \
} \
} while (0)
#define RCTLog(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__)
#define RCTLogInfo(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__)
#define RCTLogWarn(...) _RCTLog(RCTLOG_WARN, __VA_ARGS__)
#define RCTLogError(...) _RCTLog(RCTLOG_ERROR, __VA_ARGS__)
#define RCTLogMustFix(...) _RCTLog(RCTLOG_MUSTFIX, __VA_ARGS__)
#define RCTLogFormat(...) _RCTLogFormat(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__)
#define RCTLogFormatString(...) _RCTLogFormatString(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__)
static inline NSString *_RCTLogPreamble(const char *file, int lineNumber, const char *funcName) {
NSString *threadName = [[NSThread currentThread] name];
NSString *fileName=[[NSString stringWithUTF8String:file] lastPathComponent];
if (!threadName || threadName.length <= 0) {
threadName = [NSString stringWithFormat:@"%p", [NSThread currentThread]];
}
return [NSString stringWithFormat:@"[RCTLog][tid:%@][%@:%d]>", threadName, fileName, lineNumber];
}
// Returns array of objects. First arg is a simple string to print, remaining args are objects to pass through to the debugger so they are
// inspectable in the console.
static inline NSArray *_RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5);
static inline NSArray *_RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...)
{
va_list args;
va_start(args, format);
NSString *preamble = _RCTLogPreamble(file, lineNumber, funcName);
// Pull out NSObjects so we can pass them through as inspectable objects to the js debugger
NSArray *formatParts = [format componentsSeparatedByString:@"%"];
NSMutableArray *objects = [NSMutableArray arrayWithObject:preamble];
BOOL valid = YES;
for (int i = 0; i < formatParts.count; i++) {
if (i == 0) { // first part is always a string
[objects addObject:formatParts[i]];
} else {
if (valid && [formatParts[i] length] && [formatParts[i] characterAtIndex:0] == '@') {
id obj = va_arg(args, id);
if (obj) {
[objects addObject:obj];
} else {
[objects addObject:@"null"];
}
[objects addObject:[formatParts[i] substringFromIndex:1]]; // remove formatting char
} else {
// We could determine the type (double, int?) of the va_arg by parsing the formatPart, but for now we just bail.
valid = NO;
[objects addObject:[NSString stringWithFormat:@"unknown object for %%%@", formatParts[i]]];
}
}
}
va_end(args);
va_start(args, format);
NSString *strOut = [preamble stringByAppendingString:[[NSString alloc] initWithFormat:format arguments:args]];
va_end(args);
NSMutableArray *objectsOut = [NSMutableArray arrayWithObject:strOut];
[objectsOut addObjectsFromArray:objects];
return objectsOut;
}
static inline NSString *_RCTLogFormatString(const char *, int, const char *, NSString *, ...) NS_FORMAT_FUNCTION(4,5);
static inline NSString *_RCTLogFormatString(const char *file, int lineNumber, const char *funcName, NSString *format, ...)
{
va_list args;
va_start (args, format);
NSString *body = [[NSString alloc] initWithFormat:format arguments:args];
va_end (args);
return [NSString stringWithFormat:@"%@ %@", _RCTLogPreamble(file, lineNumber, funcName), body];
}
#ifdef __cplusplus
extern "C" {
#endif
NSString *_RCTLogObjects(NSArray *objects, NSString *level);
#ifdef __cplusplus
}
#endif
typedef void (^RCTLogFunction)(NSString *format, NSString *str);
void RCTInjectLogFunction(RCTLogFunction func);

33
ReactKit/Base/RCTLog.m Normal file
View File

@@ -0,0 +1,33 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTLog.h"
#import "RCTBridge.h"
static RCTLogFunction injectedLogFunction;
void RCTInjectLogFunction(RCTLogFunction func) {
injectedLogFunction = func;
}
// TODO (#5906496): // kinda ugly that this is tied to RCTBridge
NSString *_RCTLogObjects(NSArray *objects, NSString *level)
{
NSString *str = objects[0];
#if TARGET_IPHONE_SIMULATOR
if ([RCTBridge hasValidJSExecutor]) {
fprintf(stderr, "%s\n", [str UTF8String]); // don't print timestamps and other junk
[RCTBridge log:objects level:level];
} else
#endif
{
// Print normal errors with timestamps when not in simulator.
// Non errors are already compiled out above, so log as error here.
if (injectedLogFunction) {
injectedLogFunction(@">\n %@", str);
} else {
NSLog(@">\n %@", str);
}
}
return str;
}

View File

@@ -0,0 +1,102 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <Foundation/Foundation.h>
/**
* All of this will be replaced with an auto-generated bridge.
*/
typedef NS_ENUM(NSUInteger, RCTJSModuleIDs) {
RCTModuleIDReactIOSEventEmitter,
RCTModuleIDJSTimers, // JS timer tracking module
RCTModuleIDReactIOS,
RCTModuleIDBundler,
RCTModuleIDDimensions,
RCTModuleIDDeviceEventEmitter,
RCTModuleIDNativeAppEventEmitter,
RCTModuleIDRenderingPerf,
};
/**
* JS module `RCTIOSEventEmitter`.
*/
typedef NS_ENUM(NSUInteger, RCTEventEmitterRemoteMethodIDs) {
RCTEventEmitterReceiveEvent = 0,
RCTEventEmitterReceiveTouches
};
/**
* `RCTEventEmitter`: Encoding of parameters.
*/
typedef NS_ENUM(NSUInteger, RCTEventType) {
RCTEventTap = 1,
RCTEventVisibleCellsChange,
RCTEventNavigateBack,
RCTEventNavRightButtonTap,
RCTEventChange,
RCTEventTextFieldDidFocus,
RCTEventTextFieldWillBlur,
RCTEventTextFieldSubmitEditing,
RCTEventTextFieldEndEditing,
RCTEventTextInput,
RCTEventLongPress,
RCTEventTouchStart,
RCTEventTouchMove,
RCTEventTouchCancel,
RCTEventTouchEnd,
RCTEventScrollBeginDrag,
RCTEventScroll,
RCTEventScrollEndDrag,
RCTEventSelectionChange,
RCTEventMomentumScrollBegin,
RCTEventMomentumScrollEnd,
RCTEventPullToRefresh,
RCTEventScrollAnimationEnd,
RCTEventLoadingStart,
RCTEventLoadingFinish,
RCTEventLoadingError,
RCTEventNavigationProgress,
};
typedef NS_ENUM(NSUInteger, RCTKeyCode) {
RCTKeyCodeBackspace = 8,
RCTKeyCodeReturn = 13,
};
/**
* JS timer tracking module.
*/
typedef NS_ENUM(NSUInteger, RCTJSTimersMethodIDs) {
RCTJSTimersCallTimers = 0
};
typedef NS_ENUM(NSUInteger, RCTReactIOSMethodIDs) {
RCTReactIOSUnmountComponentAtNodeAndRemoveContainer = 0,
};
typedef NS_ENUM(NSUInteger, RCTBundlerMethodIDs) {
RCTBundlerRunApplication = 0
};
typedef NS_ENUM(NSUInteger, RCTDimensionsMethodIDs) {
RCTDimensionsSet = 0
};
typedef NS_ENUM(NSUInteger, RCTRenderingPerfMethodIDs) {
RCTRenderingPerfToggle = 0,
};
typedef NS_ENUM(NSUInteger, RCTDeviceEventEmitterMethodIDs) {
RCTDeviceEventEmitterEmit = 0
};
typedef NS_ENUM(NSUInteger, RCTNativeAppEventEmitterMethodIDs) {
RCTNativeAppEventEmitterEmit = 0
};
@interface RCTModuleIDs : NSObject
+ (NSDictionary *)config;
@end

View File

@@ -0,0 +1,102 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTModuleIDs.h"
@implementation RCTModuleIDs
/**
* Configures invocations from IOS -> JS. Simply passes the name of the key in
* the configuration object `require('ReactIOSEventEmitter')`.
*/
+ (NSDictionary *)config
{
return @{
@"Dimensions": @{
@"moduleID": @(RCTModuleIDDimensions),
@"methods": @{
@"set": @{
@"methodID": @(RCTDimensionsSet),
@"type": @"local"
},
}
},
@"RCTRenderingPerf": @{
@"moduleID": @(RCTModuleIDRenderingPerf),
@"methods": @{
@"toggle": @{
@"methodID": @(RCTRenderingPerfToggle),
@"type": @"local"
},
}
},
@"RCTDeviceEventEmitter": @{
@"moduleID": @(RCTModuleIDDeviceEventEmitter),
@"methods": @{
@"emit": @{
@"methodID": @(RCTDeviceEventEmitterEmit),
@"type": @"local"
},
}
},
@"RCTEventEmitter": @{
@"moduleID": @(RCTModuleIDReactIOSEventEmitter),
@"methods": @{
@"receiveEvent": @{
@"methodID": @(RCTEventEmitterReceiveEvent),
@"type": @"local"
},
@"receiveTouches": @{
@"methodID": @(RCTEventEmitterReceiveTouches),
@"type": @"local"
},
}
},
@"RCTNativeAppEventEmitter": @{
@"moduleID": @(RCTModuleIDNativeAppEventEmitter),
@"methods": @{
@"emit": @{
@"methodID": @(RCTDeviceEventEmitterEmit),
@"type": @"local"
},
}
},
@"RCTJSTimers": @{
@"moduleID": @(RCTModuleIDJSTimers),
@"methods": @{
// Last argument is the callback.
@"callTimers": @{
@"methodID": @(RCTJSTimersCallTimers),
@"type": @"local"
},
}
},
@"ReactIOS": @{
@"moduleID": @(RCTModuleIDReactIOS),
@"methods": @{
@"unmountComponentAtNodeAndRemoveContainer": @{
@"methodID": @(RCTReactIOSUnmountComponentAtNodeAndRemoveContainer),
@"type": @"local"
},
}
},
@"Bundler": @{
@"moduleID": @(RCTModuleIDBundler),
@"methods": @{
@"runApplication": @{
@"methodID": @(RCTBundlerRunApplication),
@"type": @"local"
}
}
}
};
}
@end

View File

@@ -0,0 +1,19 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <Foundation/Foundation.h>
@protocol RCTNativeModule;
@interface RCTModuleMethod : NSObject
- (instancetype)initWithSelector:(SEL)selector
JSMethodName:(NSString *)JSMethodName
arity:(NSUInteger)arity
blockArgumentIndexes:(NSIndexSet *)blockArgumentIndexes;
@property (readonly, nonatomic, assign) SEL selector;
@property (readonly, nonatomic, copy) NSString *JSMethodName;
@property (readonly, nonatomic, assign) NSUInteger arity;
@property (readonly, nonatomic, copy) NSIndexSet *blockArgumentIndexes;
@end

View File

@@ -0,0 +1,35 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTModuleMethod.h"
@implementation RCTModuleMethod
- (instancetype)initWithSelector:(SEL)selector
JSMethodName:(NSString *)JSMethodName
arity:(NSUInteger)arity
blockArgumentIndexes:(NSIndexSet *)blockArgumentIndexes
{
if ((self = [super init])) {
_selector = selector;
_JSMethodName = [JSMethodName copy];
_arity = arity;
_blockArgumentIndexes = [blockArgumentIndexes copy];
}
return self;
}
- (NSString *)description
{
NSString *blocks = @"no block args";
if (self.blockArgumentIndexes.count > 0) {
NSMutableString *indexString = [NSMutableString string];
[self.blockArgumentIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[indexString appendFormat:@", %tu", idx];
}];
blocks = [NSString stringWithFormat:@"block args at %@", [indexString substringFromIndex:2]];
}
return [NSString stringWithFormat:@"<%@: %p; exports -%@ as %@; %@>", NSStringFromClass(self.class), self, NSStringFromSelector(self.selector), self.JSMethodName, blocks];
}
@end

View File

@@ -0,0 +1,33 @@
// Copyright 2004-present Facebook. All Rights Reserved.
// Copyright 2013-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
@class RCTMultiTouchGestureRecognizer;
@protocol RCTMultiTouchGestureRecognizerListener <NSObject>
- (void)handleTouchesStarted:(NSSet *)startedTouches
forMultiGestureRecognizer:(RCTMultiTouchGestureRecognizer *)multiTouchGestureRecognizer
withEvent:(UIEvent *)event;
- (void)handleTouchesMoved:(NSSet *)movedTouches
forMultiGestureRecognizer:(RCTMultiTouchGestureRecognizer *)multiTouchGestureRecognizer
withEvent:(UIEvent *)event;
- (void)handleTouchesEnded:(NSSet *)endedTouches
forMultiGestureRecognizer:(RCTMultiTouchGestureRecognizer *)multiTouchGestureRecognizer
withEvent:(UIEvent *)event;
- (void)handleTouchesCancelled:(NSSet *)cancelledTouches
forMultiGestureRecognizer:(RCTMultiTouchGestureRecognizer *)multiTouchGestureRecognizer
withEvent:(UIEvent *)event;
@end
@interface RCTMultiTouchGestureRecognizer : UIGestureRecognizer
@property (nonatomic, weak) id<RCTMultiTouchGestureRecognizerListener> touchEventDelegate;
@end

View File

@@ -0,0 +1,103 @@
// Copyright 2004-present Facebook. All Rights Reserved.
// Copyright 2013-present Facebook. All Rights Reserved.
#import "RCTMultiTouchGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#import "RCTLog.h"
@implementation RCTMultiTouchGestureRecognizer
- (void)touchesBegan:(NSSet *)startedTouches withEvent:(UIEvent *)event {
[super touchesBegan:startedTouches withEvent:event];
if (!self.touchEventDelegate) {
RCTLogError(@"No Touch Delegate for Simple Gesture Recognizer");
return;
}
self.state = UIGestureRecognizerStateBegan;
[self.touchEventDelegate handleTouchesStarted:startedTouches
forMultiGestureRecognizer:self
withEvent:event];
}
- (void)touchesMoved:(NSSet *)movedTouches withEvent:(UIEvent *)event {
[super touchesMoved:movedTouches withEvent:event];
if (self.state == UIGestureRecognizerStateFailed) {
return;
}
[self.touchEventDelegate handleTouchesMoved:movedTouches
forMultiGestureRecognizer:self
withEvent:event];
}
- (void)touchesEnded:(NSSet *)endedTouches withEvent:(UIEvent *)event {
[super touchesEnded:endedTouches withEvent:event];
[self.touchEventDelegate handleTouchesEnded:endedTouches
forMultiGestureRecognizer:self
withEvent:event];
// These may be a different set than the total set of touches.
NSSet *touches = [event touchesForGestureRecognizer:self];
BOOL hasEnded = [self _allTouchesAreCanceledOrEnded:touches];
if (hasEnded) {
self.state = UIGestureRecognizerStateEnded;
} else if ([self _anyTouchesChanged:touches]) {
self.state = UIGestureRecognizerStateChanged;
}
}
- (void)touchesCancelled:(NSSet *)cancelledTouches withEvent:(UIEvent *)event {
[super touchesCancelled:cancelledTouches withEvent:event];
[self.touchEventDelegate handleTouchesCancelled:cancelledTouches
forMultiGestureRecognizer:self
withEvent:event];
// These may be a different set than the total set of touches.
NSSet *touches = [event touchesForGestureRecognizer:self];
BOOL hasCanceled = [self _allTouchesAreCanceledOrEnded:touches];
if (hasCanceled) {
self.state = UIGestureRecognizerStateFailed;
} else if ([self _anyTouchesChanged:touches]) {
self.state = UIGestureRecognizerStateChanged;
}
}
- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
{
return NO;
}
- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
return NO;
}
#pragma mark - Private
- (BOOL)_allTouchesAreCanceledOrEnded:(NSSet *)touches
{
for (UITouch *touch in touches) {
if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
return NO;
} else if (touch.phase == UITouchPhaseStationary) {
return NO;
}
}
return YES;
}
- (BOOL)_anyTouchesChanged:(NSSet *)touches
{
for (UITouch *touch in touches) {
if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
return YES;
}
}
return NO;
}
@end

16
ReactKit/Base/RCTRedBox.h Normal file
View File

@@ -0,0 +1,16 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
@interface RCTRedBox : NSObject
+ (instancetype)sharedInstance;
- (void)showErrorMessage:(NSString *)message;
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details;
- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack;
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack;
- (void)dismiss;
@end

294
ReactKit/Base/RCTRedBox.m Normal file
View File

@@ -0,0 +1,294 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTRedBox.h"
#import "RCTRootView.h"
#import "RCTUtils.h"
@interface RCTRedBoxWindow : UIWindow <UITableViewDelegate, UITableViewDataSource>
@end
@implementation RCTRedBoxWindow
{
UIView *_rootView;
UITableView *_stackTraceTableView;
NSString *_lastErrorMessage;
NSArray *_lastStackTrace;
UITableViewCell *_cachedMessageCell;
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.windowLevel = UIWindowLevelStatusBar + 5;
self.backgroundColor = [UIColor colorWithRed:0.8 green:0 blue:0 alpha:1];
self.hidden = YES;
UIViewController *rootController = [[UIViewController alloc] init];
self.rootViewController = rootController;
_rootView = rootController.view;
_rootView.backgroundColor = [UIColor clearColor];
const CGFloat buttonHeight = 60;
CGRect detailsFrame = _rootView.bounds;
detailsFrame.size.height -= buttonHeight;
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
_stackTraceTableView.delegate = self;
_stackTraceTableView.dataSource = self;
_stackTraceTableView.backgroundColor = [UIColor clearColor];
_stackTraceTableView.separatorColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3];
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[_rootView addSubview:_stackTraceTableView];
UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeCustom];
dismissButton.titleLabel.font = [UIFont systemFontOfSize:14];
[dismissButton setTitle:@"Dismiss (ESC)" forState:UIControlStateNormal];
[dismissButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5] forState:UIControlStateNormal];
[dismissButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[dismissButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside];
UIButton *reloadButton = [UIButton buttonWithType:UIButtonTypeCustom];
reloadButton.titleLabel.font = [UIFont systemFontOfSize:14];
[reloadButton setTitle:@"Reload JS (\u2318R)" forState:UIControlStateNormal];
[reloadButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5] forState:UIControlStateNormal];
[reloadButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[reloadButton addTarget:self action:@selector(reload) forControlEvents:UIControlEventTouchUpInside];
CGFloat buttonWidth = self.bounds.size.width / 2;
dismissButton.frame = CGRectMake(0, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight);
reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight);
[_rootView addSubview:dismissButton];
[_rootView addSubview:reloadButton];
}
return self;
}
- (void)openStackFrameInEditor:(NSDictionary *)stackFrame
{
NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, nil) dataUsingEncoding:NSUTF8StringEncoding];
NSString *postLength = [NSString stringWithFormat:@"%lu", (unsigned long)[stackFrameJSON length]];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
request.URL = [NSURL URLWithString:@"http://localhost:8081/open-stack-frame"];
request.HTTPMethod = @"POST";
request.HTTPBody = stackFrameJSON;
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:nil];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow
{
_lastStackTrace = stack;
_lastErrorMessage = message;
_cachedMessageCell = [self reuseCell:nil forErrorMessage:message];
[_stackTraceTableView reloadData];
[_stackTraceTableView setNeedsLayout];
if (self.hidden && shouldShow) {
[_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
[self makeKeyAndVisible];
[self becomeFirstResponder];
}
}
- (void)dismiss
{
self.hidden = YES;
}
- (void)reload
{
[RCTRootView reloadAll];
[self dismiss];
}
#pragma mark - TableView
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return section == 0 ? 1 : [_lastStackTrace count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([indexPath section] == 0) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"];
return [self reuseCell:cell forErrorMessage:_lastErrorMessage];
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
NSUInteger index = [indexPath row];
NSDictionary *stackFrame = _lastStackTrace[index];
return [self reuseCell:cell forStackFrame:stackFrame];
}
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message
{
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"msg-cell"];
cell.textLabel.textColor = [UIColor whiteColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:16];
cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
cell.textLabel.numberOfLines = 0;
cell.detailTextLabel.textColor = [UIColor whiteColor];
cell.backgroundColor = [UIColor clearColor];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
cell.textLabel.text = message;
return cell;
}
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(NSDictionary *)stackFrame
{
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
cell.textLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14];
cell.detailTextLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11];
cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
cell.backgroundColor = [UIColor clearColor];
cell.selectedBackgroundView = [[UIView alloc] init];
cell.selectedBackgroundView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2];
}
cell.textLabel.text = stackFrame[@"methodName"];
cell.detailTextLabel.text = cell.detailTextLabel.text = [NSString stringWithFormat:@"%@:%@", [stackFrame[@"file"] lastPathComponent], stackFrame[@"lineNumber"]];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([indexPath section] == 0) {
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:16],
NSParagraphStyleAttributeName: paragraphStyle};
CGRect boundingRect = [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
return ceil(boundingRect.size.height) + 40;
} else {
return 44;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([indexPath section] == 1) {
NSUInteger row = [indexPath row];
NSDictionary *stackFrame = _lastStackTrace[row];
[self openStackFrameInEditor:stackFrame];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - Key commands
- (NSArray *)keyCommands
{
// NOTE: We could use RCTKeyCommands for this, but since
// we control this window, we can use the standard, non-hacky
// mechanism instead
return @[
// Dismiss red box
[UIKeyCommand keyCommandWithInput:UIKeyInputEscape
modifierFlags:0
action:@selector(dismiss)],
// Reload
[UIKeyCommand keyCommandWithInput:@"r"
modifierFlags:UIKeyModifierCommand
action:@selector(reload)]
];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
@end
@implementation RCTRedBox
{
RCTRedBoxWindow *_window;
}
+ (instancetype)sharedInstance
{
static RCTRedBox *_sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[RCTRedBox alloc] init];
});
return _sharedInstance;
}
- (void)showErrorMessage:(NSString *)message
{
[self showErrorMessage:message withStack:nil showIfHidden:YES];
}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
{
NSString *combinedMessage = message;
if (details) {
combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details];
}
[self showErrorMessage:combinedMessage];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack
{
[self showErrorMessage:message withStack:stack showIfHidden:YES];
}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack
{
[self showErrorMessage:message withStack:stack showIfHidden:NO];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow
{
#if DEBUG
dispatch_block_t block = ^{
if (!_window) {
_window = [[RCTRedBoxWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
}
[_window showErrorMessage:message withStack:stack showIfHidden:shouldShow];
};
if ([NSThread isMainThread]) {
block();
} else {
dispatch_async(dispatch_get_main_queue(), block);
}
#endif
}
- (void)dismiss
{
[_window dismiss];
}
@end

View File

@@ -0,0 +1,20 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
@protocol RCTJavaScriptExecutor;
@interface RCTRootView : UIView
@property (nonatomic, strong) NSURL *scriptURL;
@property (nonatomic, copy) NSString *moduleName;
@property (nonatomic, copy) NSDictionary *initialProperties;
@property (nonatomic, strong) id<RCTJavaScriptExecutor> executor;
/**
* Reload this root view, or all root views, respectively.
*/
- (void)reload;
+ (void)reloadAll;
@end

190
ReactKit/Base/RCTRootView.m Normal file
View File

@@ -0,0 +1,190 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTRootView.h"
#import "RCTBridge.h"
#import "RCTContextExecutor.h"
#import "RCTJavaScriptAppEngine.h"
#import "RCTJavaScriptEventDispatcher.h"
#import "RCTModuleIDs.h"
#import "RCTRedBox.h"
#import "RCTShadowView.h"
#import "RCTSparseArray.h"
#import "RCTTouchHandler.h"
#import "RCTUIManager.h"
#import "RCTUtils.h"
#import "RCTViewManager.h"
#import "UIView+ReactKit.h"
#import "RCTKeyCommands.h"
NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification";
@implementation RCTRootView
{
dispatch_queue_t _shadowQueue;
RCTBridge *_bridge;
RCTJavaScriptAppEngine *_appEngine;
RCTTouchHandler *_touchHandler;
}
+ (void)initialize
{
#if DEBUG
// Register Cmd-R as a global refresh key
[[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"r"
modifierFlags:UIKeyModifierCommand
action:^(UIKeyCommand *command) {
[self reloadAll];
}];
#endif
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (!self) return nil;
[self setUp];
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (!self) return nil;
[self setUp];
return self;
}
- (void)setUp
{
// TODO: does it make sense to do this here? What if there's more than one host view?
_shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL);
// Every root view that is created must have a unique react tag.
// Numbering of these tags goes from 1, 11, 21, 31, etc
static NSInteger rootViewTag = 1;
self.reactTag = @(rootViewTag);
rootViewTag += 10;
// Add reload observer
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(reload)
name:RCTRootViewReloadNotification
object:nil];
self.backgroundColor = [UIColor whiteColor];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)bundleFinishedLoading:(NSError *)error
{
if (error != nil) {
[[RCTRedBox sharedInstance] showErrorMessage:error.localizedDescription withDetails:error.localizedFailureReason];
} else {
[_bridge.uiManager registerRootView:self];
NSString *moduleName = _moduleName ?: @"";
NSDictionary *appParameters = @{
@"rootTag": self.reactTag ?: @0,
@"initialProps": self.initialProperties ?: @{},
};
[_appEngine.bridge enqueueJSCall:RCTModuleIDBundler
methodID:RCTBundlerRunApplication
args:@[moduleName, appParameters]];
}
}
- (void)loadBundle
{
// Clear view
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
if (!_scriptURL) {
return;
}
__weak typeof(self) weakSelf = self;
RCTJavaScriptCompleteBlock callback = ^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf bundleFinishedLoading:error];
});
};
[_executor invalidate];
[_appEngine invalidate];
[_bridge invalidate];
_executor = [[RCTContextExecutor alloc] init];
_bridge = [[RCTBridge alloc] initWithJavaScriptExecutor:_executor
shadowQueue:_shadowQueue
javaScriptModulesConfig:[RCTModuleIDs config]];
_appEngine = [[RCTJavaScriptAppEngine alloc] initWithBridge:_bridge];
_touchHandler = [[RCTTouchHandler alloc] initWithEventDispatcher:_bridge.eventDispatcher rootView:self];
[_appEngine loadBundleAtURL:_scriptURL useCache:NO onComplete:callback];
}
- (void)setScriptURL:(NSURL *)scriptURL
{
if ([_scriptURL isEqual:scriptURL]) {
return;
}
_scriptURL = scriptURL;
[self loadBundle];
}
- (void)setExecutor:(id<RCTJavaScriptExecutor>)executor
{
RCTAssert(!_bridge, @"You may only change the Javascript Executor prior to loading a script bundle.");
_executor = executor;
}
- (BOOL)isReactRootView
{
return YES;
}
- (void)reload
{
[RCTJavaScriptAppEngine resetCacheForBundleAtURL:_scriptURL];
[self loadBundle];
}
+ (void)reloadAll
{
[[NSNotificationCenter defaultCenter] postNotificationName:RCTRootViewReloadNotification object:nil];
}
#pragma mark - Key commands
- (NSArray *)keyCommands
{
return @[
// Reload
[UIKeyCommand keyCommandWithInput:@"r"
modifierFlags:UIKeyModifierCommand
action:@selector(reload)]
];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
@end

View File

@@ -0,0 +1,24 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
@class RCTJavaScriptEventDispatcher;
/**
* Handles throttling of scroll events that are dispatched to JavaScript.
*/
@interface RCTScrollDispatcher : NSObject
@property (nonatomic, readwrite, assign) NSInteger throttleScrollCallbackMS;
- (instancetype)initWithEventDispatcher:(RCTJavaScriptEventDispatcher *)dispatcher;
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag;
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag;
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag;
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag;
@end

View File

@@ -0,0 +1,131 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTScrollDispatcher.h"
#import "RCTBridge.h"
#import "RCTEventExtractor.h"
#import "RCTJavaScriptEventDispatcher.h"
#import "UIView+ReactKit.h"
@implementation RCTScrollDispatcher
{
RCTJavaScriptEventDispatcher *_eventDispatcher;
NSTimeInterval _lastScrollDispatchTime;
BOOL _allowNextScrollNoMatterWhat;
NSMutableDictionary *_cachedChildFrames;
CGPoint _lastContentOffset;
}
- (instancetype)initWithEventDispatcher:(RCTJavaScriptEventDispatcher *)dispatcher
{
if (self = [super init]) {
_eventDispatcher = dispatcher;
_throttleScrollCallbackMS = 0;
_lastScrollDispatchTime = CACurrentMediaTime();
_cachedChildFrames = [NSMutableDictionary new];
}
return self;
}
- (NSArray *)_getUpdatedChildFrames:(UIScrollView *)scrollView forUpdateKey:(NSString *)updateKey
{
NSArray *children = [scrollView.subviews[0] reactSubviews];
NSMutableArray *updatedChildFrames = [NSMutableArray new];
NSMutableArray *cachedFrames = _cachedChildFrames[updateKey];
if (!cachedFrames) {
cachedFrames = [[NSMutableArray alloc] initWithCapacity:children.count];
_cachedChildFrames[updateKey] = cachedFrames;
}
for (int ii = 0; ii < children.count; ii++) {
CGRect newFrame = [children[ii] frame];
if (cachedFrames.count <= ii || !CGRectEqualToRect(newFrame, [cachedFrames[ii] CGRectValue])) {
[updatedChildFrames addObject:
@{
@"index": @(ii),
@"x": @(newFrame.origin.x),
@"y": @(newFrame.origin.y),
@"width": @(newFrame.size.width),
@"height": @(newFrame.size.height),
}];
NSValue *frameObj = [NSValue valueWithCGRect:newFrame];
if (cachedFrames.count <= ii) {
[cachedFrames addObject:frameObj];
} else {
cachedFrames[ii] = frameObj;
}
}
}
return updatedChildFrames;
}
- (void)_dispatchScroll:(UIScrollView *)scrollView forUpdateKey:(NSString *)updateKey reactTag:(NSNumber *)reactTag
{
NSTimeInterval now = CACurrentMediaTime();
NSTimeInterval dt = now - _lastScrollDispatchTime;
NSMutableDictionary *mutableNativeObj = [[RCTEventExtractor scrollEventObject:scrollView reactTag:reactTag] mutableCopy];
if (updateKey) {
NSArray *updatedChildFrames = [self _getUpdatedChildFrames:scrollView forUpdateKey:updateKey];
if (updatedChildFrames.count > 0) {
mutableNativeObj[@"updatedChildFrames"] = updatedChildFrames;
}
}
mutableNativeObj[@"velocity"] = @{
@"x": @((scrollView.contentOffset.x - _lastContentOffset.x) / dt),
@"y": @((scrollView.contentOffset.y - _lastContentOffset.y) / dt),
};
[_eventDispatcher sendEventWithArgs:[RCTEventExtractor eventArgs:reactTag
type:RCTEventScroll
nativeEventObj:mutableNativeObj]];
_lastScrollDispatchTime = now;
_lastContentOffset = scrollView.contentOffset;
_allowNextScrollNoMatterWhat = NO;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag
{
[_eventDispatcher sendEventWithArgs:[RCTEventExtractor eventArgs:reactTag
type:RCTEventScrollAnimationEnd
nativeEventObj:[RCTEventExtractor scrollEventObject:scrollView reactTag:reactTag]]];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag
{
NSTimeInterval now = CACurrentMediaTime();
NSTimeInterval throttleScrollCallbackSeconds = _throttleScrollCallbackMS / 1000.0f;
if (_allowNextScrollNoMatterWhat ||
(_throttleScrollCallbackMS != 0 && throttleScrollCallbackSeconds < (now - _lastScrollDispatchTime))) {
[self _dispatchScroll:scrollView forUpdateKey:@"didScroll" reactTag:reactTag];
}
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag
{
[_eventDispatcher sendEventWithArgs:[RCTEventExtractor eventArgs:reactTag
type:RCTEventMomentumScrollBegin
nativeEventObj:[RCTEventExtractor scrollEventObject:scrollView reactTag:reactTag]]];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag
{
[_eventDispatcher sendEventWithArgs:[RCTEventExtractor eventArgs:reactTag
type:RCTEventMomentumScrollEnd
nativeEventObj:[RCTEventExtractor scrollEventObject:scrollView reactTag:reactTag]]];
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag
{
_allowNextScrollNoMatterWhat = YES;
[_eventDispatcher sendEventWithArgs:[RCTEventExtractor eventArgs:reactTag
type:RCTEventScrollBeginDrag
nativeEventObj:[RCTEventExtractor scrollEventObject:scrollView reactTag:reactTag]]];
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView reactTag:(NSNumber *)reactTag
{
[_eventDispatcher sendEventWithArgs:[RCTEventExtractor eventArgs:reactTag
type:RCTEventScrollEndDrag
nativeEventObj:[RCTEventExtractor scrollEventObject:scrollView reactTag:reactTag]]];
}
@end

View File

@@ -0,0 +1,18 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
/**
* Contains any methods related to scrolling. Any `RCTView` that has scrolling
* features should implement these methods.
*/
@protocol RCTScrollableProtocol
@property (nonatomic, readwrite, weak) NSObject<UIScrollViewDelegate> *nativeMainScrollDelegate;
@property (nonatomic, readonly) CGSize contentSize;
- (void)scrollToOffset:(CGPoint)offset;
- (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated;
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated;
@end

View File

@@ -0,0 +1,30 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <Foundation/Foundation.h>
@interface RCTSparseArray : NSObject <NSCopying>
- (instancetype)init;
- (instancetype)initWithCapacity:(NSUInteger)capacity;
- (instancetype)initWithSparseArray:(RCTSparseArray *)sparseArray;
+ (instancetype)sparseArray;
+ (instancetype)sparseArrayWithCapacity:(NSUInteger)capacity;
+ (instancetype)sparseArrayWithSparseArray:(RCTSparseArray *)sparseArray;
// Use nil object to remove at idx.
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx;
- (id)objectAtIndexedSubscript:(NSUInteger)idx;
// Use nil obj to remove at key.
- (void)setObject:(id)obj forKeyedSubscript:(NSNumber *)key;
- (id)objectForKeyedSubscript:(NSNumber *)key;
@property (readonly, nonatomic) NSUInteger count;
@property (readonly, nonatomic, copy) NSArray *allIndexes;
@property (readonly, nonatomic, copy) NSArray *allObjects;
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSNumber *idx, BOOL *stop))block;
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id obj, NSNumber *idx, BOOL *stop))block;
@end

View File

@@ -0,0 +1,116 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTSparseArray.h"
@implementation RCTSparseArray
{
NSMutableDictionary *_storage;
}
- (instancetype)init
{
return [self initWithCapacity:0];
}
- (instancetype)initWithCapacity:(NSUInteger)capacity
{
if (self = [super init]) {
_storage = [NSMutableDictionary dictionaryWithCapacity:capacity];
}
return self;
}
- (instancetype)initWithSparseArray:(RCTSparseArray *)sparseArray
{
if (self = [super init]) {
_storage = [sparseArray->_storage copy];
}
return self;
}
- (instancetype)initWithStorage:(NSDictionary *)storage
{
if ((self = [super init])) {
_storage = [storage copy];
}
return self;
}
+ (instancetype)sparseArray
{
return [[self alloc] init];
}
+ (instancetype)sparseArrayWithCapacity:(NSUInteger)capacity
{
return [[self alloc] initWithCapacity:capacity];
}
+ (instancetype)sparseArrayWithSparseArray:(RCTSparseArray *)sparseArray
{
return [[self alloc] initWithSparseArray:sparseArray];
}
- (id)objectAtIndexedSubscript:(NSUInteger)idx
{
return _storage[@(idx)];
}
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx
{
self[@(idx)] = obj;
}
- (id)objectForKeyedSubscript:(NSNumber *)key
{
return _storage[key];
}
- (void)setObject:(id)obj forKeyedSubscript:(NSNumber *)key
{
if (obj) {
_storage[key] = obj;
} else {
[_storage removeObjectForKey:key];
}
}
- (NSUInteger)count
{
return _storage.count;
}
- (NSArray *)allIndexes
{
return _storage.allKeys;
}
- (NSArray *)allObjects
{
return _storage.allValues;
}
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSNumber *idx, BOOL *stop))block
{
NSParameterAssert(block != nil);
[_storage enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
block(obj, key, stop);
}];
}
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id obj, NSNumber *idx, BOOL *stop))block
{
NSParameterAssert(block != nil);
[_storage enumerateKeysAndObjectsWithOptions:opts usingBlock:^(NSNumber *key, id obj, BOOL *stop) {
block(obj, key, stop);
}];
}
- (id)copyWithZone:(NSZone *)zone
{
return [[[self class] allocWithZone:zone] initWithSparseArray:self];
}
@end

View File

@@ -0,0 +1,32 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
@class RCTJavaScriptEventDispatcher;
@interface RCTTouchHandler : NSObject
- (instancetype)initWithEventDispatcher:(RCTJavaScriptEventDispatcher *)eventDispatcher
rootView:(UIView *)rootView;
@property (nonatomic, readwrite, strong) RCTJavaScriptEventDispatcher *eventDispatcher;
/**
* Maintaining the set of active touches by the time they started touching.
*/
@property (nonatomic, readonly, strong) NSMutableArray *orderedTouches;
/**
* Array managed in parallel to `orderedTouches` tracking original `reactTag`
* for each touch. This must be kept track of because `UIKit` destroys the
* touch targets if touches are canceled and we have no other way to recover
* this information.
*/
@property (nonatomic, readonly, strong) NSMutableArray *orderedTouchStartTags;
/**
* IDs that uniquely represent a touch among all of the active touches.
*/
@property (nonatomic, readonly, strong) NSMutableArray *orderedTouchIDs;
@end

View File

@@ -0,0 +1,296 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTTouchHandler.h"
#import "RCTAssert.h"
#import "RCTEventExtractor.h"
#import "RCTJavaScriptEventDispatcher.h"
#import "RCTLog.h"
#import "RCTMultiTouchGestureRecognizer.h"
#import "RCTUIManager.h"
#import "RCTUtils.h"
#import "UIView+ReactKit.h"
@interface RCTTouchHandler () <RCTMultiTouchGestureRecognizerListener, UIGestureRecognizerDelegate>
@end
@implementation RCTTouchHandler
{
UIView *_rootView;
NSMutableArray *_gestureRecognizers;
}
- (instancetype)init
{
RCT_NOT_DESIGNATED_INITIALIZER();
}
- (instancetype)initWithEventDispatcher:(RCTJavaScriptEventDispatcher *)eventDispatcher
rootView:(UIView *)rootView
{
if (self = [super init]) {
RCTAssert(eventDispatcher != nil, @"Expect an event dispatcher");
RCTAssert(rootView != nil, @"Expect a root view");
_eventDispatcher = eventDispatcher;
_rootView = rootView;
_gestureRecognizers = [NSMutableArray new];
_orderedTouches = [[NSMutableArray alloc] init];
_orderedTouchStartTags = [[NSMutableArray alloc] init];
_orderedTouchIDs = [[NSMutableArray alloc] init];
[self _loadGestureRecognizers];
}
return self;
}
- (void)dealloc
{
[self removeGestureRecognizers];
}
#pragma mark - Gesture Recognizers
- (void)_loadGestureRecognizers
{
[self _addRecognizerForEvent:RCTEventTap];
[self _addRecognizerForEvent:RCTEventLongPress];
[self _addRecognizerForSimpleTouchEvents];
}
- (void)_addRecognizerForSimpleTouchEvents
{
RCTMultiTouchGestureRecognizer *multiTouchRecognizer =
[[RCTMultiTouchGestureRecognizer alloc] initWithTarget:self action:@selector(handleMultiTouchGesture:)];
multiTouchRecognizer.touchEventDelegate = self;
[self _addRecognizer:multiTouchRecognizer];
}
- (void)_addRecognizerForEvent:(RCTEventType)event
{
UIGestureRecognizer *recognizer = nil;
switch (event) {
case RCTEventTap:
recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
((UITapGestureRecognizer *)recognizer).numberOfTapsRequired = 1;
break;
case RCTEventVisibleCellsChange:
case RCTEventNavigateBack:
case RCTEventNavRightButtonTap:
case RCTEventChange:
case RCTEventTextFieldDidFocus:
case RCTEventTextFieldWillBlur:
case RCTEventTextFieldSubmitEditing:
case RCTEventTextFieldEndEditing:
case RCTEventScroll:
break;
case RCTEventLongPress:
recognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
break;
default:
RCTLogError(@"Unrecognized event type for gesture: %zd", event);
}
[self _addRecognizer:recognizer];
}
- (void)_addRecognizer:(UIGestureRecognizer *)recognizer
{
// `cancelsTouchesInView` is needed in order to be used as a top level event delegated recognizer. Otherwise, lower
// level components not build using RCT, will fail to recognize gestures.
recognizer.cancelsTouchesInView = NO;
recognizer.delegate = self;
[_gestureRecognizers addObject:recognizer];
[_rootView addGestureRecognizer:recognizer];
}
- (void)removeGestureRecognizers
{
for (UIGestureRecognizer *recognizer in _gestureRecognizers) {
[recognizer setDelegate:nil];
[recognizer removeTarget:nil action:NULL];
[_rootView removeGestureRecognizer:recognizer];
}
}
#pragma mark - Bookkeeping for touch indices
- (void)_recordNewTouches:(NSSet *)touches
{
for (UITouch *touch in touches) {
NSUInteger currentIndex = [_orderedTouches indexOfObject:touch];
if (currentIndex != NSNotFound) {
RCTLogError(@"Touch is already recorded. This is a critical bug.");
[_orderedTouches removeObjectAtIndex:currentIndex];
[_orderedTouchStartTags removeObjectAtIndex:currentIndex];
[_orderedTouchIDs removeObjectAtIndex:currentIndex];
}
NSNumber *touchStartTag = [RCTEventExtractor touchStartTarget:touch inView:_rootView];
// Get new, unique touch id
const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices
NSInteger touchID = ([_orderedTouchIDs.lastObject integerValue] + 1) % RCTMaxTouches;
for (NSNumber *n in _orderedTouchIDs) {
NSInteger usedID = [n integerValue];
if (usedID == touchID) {
// ID has already been used, try next value
touchID ++;
} else if (usedID > touchID) {
// If usedID > touchID, touchID must be unique, so we can stop looking
break;
}
}
[_orderedTouches addObject:touch];
[_orderedTouchStartTags addObject:touchStartTag];
[_orderedTouchIDs addObject:@(touchID)];
}
}
- (void)_recordRemovedTouches:(NSSet *)touches
{
for (UITouch *touch in touches) {
NSUInteger currentIndex = [_orderedTouches indexOfObject:touch];
if (currentIndex == NSNotFound) {
RCTLogError(@"Touch is already removed. This is a critical bug.");
} else {
[_orderedTouches removeObjectAtIndex:currentIndex];
[_orderedTouchStartTags removeObjectAtIndex:currentIndex];
[_orderedTouchIDs removeObjectAtIndex:currentIndex];
}
}
}
#pragma mark - Gesture Recognizer Delegate Callbacks
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (NSArray *)_indicesOfTouches:(NSSet *)touchSet inArray:(NSArray *)array
{
NSMutableArray *result = [[NSMutableArray alloc] init];
for (UITouch *touch in touchSet) {
[result addObject:@([array indexOfObject:touch])];
}
return result;
}
- (void)handleTouchesStarted:(NSSet *)startedTouches
forMultiGestureRecognizer:(RCTMultiTouchGestureRecognizer *)multiTouchGestureRecognizer
withEvent:(UIEvent *)event
{
// "start" has to record new touches before extracting the event.
// "end"/"cancel" needs to remove the touch *after* extracting the event.
[self _recordNewTouches:startedTouches];
NSArray *indicesOfStarts = [self _indicesOfTouches:startedTouches inArray:_orderedTouches];
NSArray *args =
[RCTEventExtractor touchEventArgsForOrderedTouches:_orderedTouches
orderedStartTags:_orderedTouchStartTags
orderedTouchIDs:_orderedTouchIDs
changedIndices:indicesOfStarts
type:RCTEventTouchStart
view:_rootView];
[_eventDispatcher sendTouchesWithArgs:args];
}
- (void)handleTouchesMoved:(NSSet *)movedTouches
forMultiGestureRecognizer:(RCTMultiTouchGestureRecognizer *)multiTouchGestureRecognizer
withEvent:(UIEvent *)event
{
NSArray *indicesOfMoves = [self _indicesOfTouches:movedTouches inArray:_orderedTouches];
NSArray *args =
[RCTEventExtractor touchEventArgsForOrderedTouches:_orderedTouches
orderedStartTags:_orderedTouchStartTags
orderedTouchIDs:_orderedTouchIDs
changedIndices:indicesOfMoves
type:RCTEventTouchMove
view:_rootView];
[_eventDispatcher sendTouchesWithArgs:args];
}
- (void)handleTouchesEnded:(NSSet *)endedTouches
forMultiGestureRecognizer:(RCTMultiTouchGestureRecognizer *)multiTouchGestureRecognizer
withEvent:(UIEvent *)event
{
NSArray *indicesOfEnds = [self _indicesOfTouches:endedTouches inArray:_orderedTouches];
NSArray *args =
[RCTEventExtractor touchEventArgsForOrderedTouches:_orderedTouches
orderedStartTags:_orderedTouchStartTags
orderedTouchIDs:_orderedTouchIDs
changedIndices:indicesOfEnds
type:RCTEventTouchEnd
view:_rootView];
[_eventDispatcher sendTouchesWithArgs:args];
[self _recordRemovedTouches:endedTouches];
}
- (void)handleTouchesCancelled:(NSSet *)cancelledTouches
forMultiGestureRecognizer:(RCTMultiTouchGestureRecognizer *)multiTouchGestureRecognizer
withEvent:(UIEvent *)event
{
NSArray *indicesOfCancels = [self _indicesOfTouches:cancelledTouches inArray:_orderedTouches];
NSArray *args =
[RCTEventExtractor touchEventArgsForOrderedTouches:_orderedTouches
orderedStartTags:_orderedTouchStartTags
orderedTouchIDs:_orderedTouchIDs
changedIndices:indicesOfCancels
type:RCTEventTouchCancel
view:_rootView];
[_eventDispatcher sendTouchesWithArgs:args];
[self _recordRemovedTouches:cancelledTouches];
}
/**
* Needed simply to be provided to the `RCTMultiTouchGestureRecognizer`. If not,
* other gestures are cancelled.
*/
- (void)handleMultiTouchGesture:(UIGestureRecognizer *)sender
{
}
- (NSDictionary *)_nativeEventForGesture:(UIGestureRecognizer *)sender
target:(UIView *)target
reactTargetView:(UIView *)reactTargetView
{
return @{
@"state": @(sender.state),
@"target": reactTargetView.reactTag,
};
}
- (void)handleTap:(UIGestureRecognizer *)sender
{
// This calculation may not be accurate when views overlap.
UIView *touchedView = sender.view;
CGPoint location = [sender locationInView:touchedView];
UIView *target = [touchedView hitTest:location withEvent:nil];
// Views outside the RCT system can be present (e.g., UITableViewCellContentView)
// they have no registry. we can safely ignore events happening on them.
if (sender.state == UIGestureRecognizerStateEnded) {
UIView *reactTargetView = [RCTUIManager closestReactAncestor:target];
if (reactTargetView) {
NSMutableDictionary *nativeEvent =[[self _nativeEventForGesture:sender target:target reactTargetView:reactTargetView] mutableCopy];
nativeEvent[@"pageX"] = @(location.x);
nativeEvent[@"pageY"] = @(location.y);
CGPoint locInView = [sender.view convertPoint:location toView:target];
nativeEvent[@"locationX"] = @(locInView.x);
nativeEvent[@"locationY"] = @(locInView.y);
[_eventDispatcher sendEventWithArgs:[RCTEventExtractor eventArgs:[reactTargetView reactTag]
type:RCTEventTap
nativeEventObj:nativeEvent]];
}
}
}
- (void)handleLongPress:(UIGestureRecognizer *)sender
{
}
@end

36
ReactKit/Base/RCTUtils.h Normal file
View File

@@ -0,0 +1,36 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#import <tgmath.h>
#import "RCTAssert.h"
// Macro to indicate when inherited initializer is not to be used
#define RCT_NOT_DESIGNATED_INITIALIZER() \
do { \
RCTAssert(NO, @"%@ is not the designated initializer for instances of %@.", NSStringFromSelector(_cmd), [self class]); \
return nil; \
} while (0)
// Utility functions for JSON object <-> string serialization/deserialization
NSString *RCTJSONStringify(id jsonObject, NSError **error);
id RCTJSONParse(NSString *jsonString, NSError **error);
// Get MD5 hash of a string
NSString *RCTMD5Hash(NSString *string);
// Get screen scale in a thread-safe way
CGFloat RCTScreenScale(void);
// Round float coordinates to nearest whole screen pixel (not point)
CGFloat RCTRoundPixelValue(CGFloat value);
CGFloat RCTCeilPixelValue(CGFloat value);
CGFloat RCTFloorPixelValue(CGFloat value);
// Get current time, for precise performance metrics
NSTimeInterval RCTTGetAbsoluteTime(void);
// Method swizzling
void RCTSwapClassMethods(Class cls, SEL original, SEL replacement);
void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement);

122
ReactKit/Base/RCTUtils.m Normal file
View File

@@ -0,0 +1,122 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTUtils.h"
#import <CommonCrypto/CommonCrypto.h>
#import <mach/mach_time.h>
#import <objc/runtime.h>
#import <UIKit/UIKit.h>
NSString *RCTJSONStringify(id jsonObject, NSError **error)
{
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:error];
return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil;
}
id RCTJSONParse(NSString *jsonString, NSError **error)
{
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error];
}
NSString *RCTMD5Hash(NSString *string)
{
const char *str = [string UTF8String];
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), result);
return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
];
}
CGFloat RCTScreenScale()
{
static CGFloat scale = -1;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
scale = [UIScreen mainScreen].scale;
});
} else {
scale = [UIScreen mainScreen].scale;
}
});
return scale;
}
CGFloat RCTRoundPixelValue(CGFloat value)
{
CGFloat scale = RCTScreenScale();
return round(value * scale) / scale;
}
CGFloat RCTCeilPixelValue(CGFloat value)
{
CGFloat scale = RCTScreenScale();
return ceil(value * scale) / scale;
}
CGFloat RCTFloorPixelValue(CGFloat value)
{
CGFloat scale = RCTScreenScale();
return floor(value * scale) / scale;
}
NSTimeInterval RCTTGetAbsoluteTime(void)
{
static struct mach_timebase_info tb_info = {0};
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
int ret = mach_timebase_info(&tb_info);
assert(0 == ret);
});
uint64_t timeInNanoseconds = (mach_absolute_time() * tb_info.numer) / tb_info.denom;
return ((NSTimeInterval)timeInNanoseconds) / 1000000;
}
void RCTSwapClassMethods(Class cls, SEL original, SEL replacement)
{
Method originalMethod = class_getClassMethod(cls, original);
IMP originalImplementation = method_getImplementation(originalMethod);
const char *originalArgTypes = method_getTypeEncoding(originalMethod);
Method replacementMethod = class_getClassMethod(cls, replacement);
IMP replacementImplementation = method_getImplementation(replacementMethod);
const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes))
{
class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes);
}
else
{
method_exchangeImplementations(originalMethod, replacementMethod);
}
}
void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement)
{
Method originalMethod = class_getInstanceMethod(cls, original);
IMP originalImplementation = method_getImplementation(originalMethod);
const char *originalArgTypes = method_getTypeEncoding(originalMethod);
Method replacementMethod = class_getInstanceMethod(cls, replacement);
IMP replacementImplementation = method_getImplementation(replacementMethod);
const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes))
{
class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes);
}
else
{
method_exchangeImplementations(originalMethod, replacementMethod);
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2004-present Facebook. All Rights Reserved.
/**
* Logical node in a tree of application components. Both `ShadowView`s and
* `UIView+ReactKit`s conform to this. Allows us to write utilities that
* reason about trees generally.
*/
@protocol RCTViewNodeProtocol <NSObject>
@property (nonatomic, strong) NSNumber *reactTag;
- (void)insertReactSubview:(id<RCTViewNodeProtocol>)subview atIndex:(NSInteger)atIndex;
- (void)removeReactSubview:(id<RCTViewNodeProtocol>)subview;
- (NSMutableArray *)reactSubviews;
// View is an RCTRootView
- (BOOL)isReactRootView;
@optional
// TODO: Deprecate this
- (void)reactBridgeDidFinishTransaction;
// Invoked when react determines that the view will be removed from the view
// hierarchy and never replaced.
- (void)reactWillDestroy;
@end