diff --git a/Examples/TicTacToe/TicTacToeApp.js b/Examples/TicTacToe/TicTacToeApp.js index ff74aa2f2..73440acd2 100755 --- a/Examples/TicTacToe/TicTacToeApp.js +++ b/Examples/TicTacToe/TicTacToeApp.js @@ -9,6 +9,7 @@ var React = require('react-native'); var { Bundler, + Image, StyleSheet, Text, TouchableHighlight, @@ -118,13 +119,22 @@ var Cell = React.createClass({ } }, + imageContents() { + switch (this.props.player) { + case 1: + return 'http://www.picgifs.com/alphabets/alphabets/children-5/alphabets-children-5-277623.gif'; + case 2: + return 'http://www.picgifs.com/alphabets/alphabets/children-5/alphabets-children-5-730492.gif'; + default: + return ''; + } + }, + render() { return ( - - {this.textContents()} - + ); diff --git a/Libraries/ReactIOS/ReactIOSEventEmitter.js b/Libraries/ReactIOS/ReactIOSEventEmitter.js index ab768c927..b539ff06e 100644 --- a/Libraries/ReactIOS/ReactIOSEventEmitter.js +++ b/Libraries/ReactIOS/ReactIOSEventEmitter.js @@ -169,12 +169,20 @@ var ReactIOSEventEmitter = merge(ReactEventEmitterMixin, { var target = nativeEvent.target; if (target !== null && target !== undefined) { if (target < ReactIOSTagHandles.tagsStartAt) { + // When we get multiple touches at the same time, only the first touch + // actually has a view attached to it. The rest of the touches do not. + // This is presumably because iOS doesn't want to send touch events to + // two views for a single multi touch. Therefore this warning is only + // appropriate when it happens to the first touch. (hence jj === 0) if (__DEV__) { - warning( - false, - 'A view is reporting that a touch occured on tag zero.' - ); + if (jj === 0) { + warning( + false, + 'A view is reporting that a touch occured on tag zero.' + ); + } } + continue; } else { rootNodeID = NodeHandle.getRootNodeID(target); } diff --git a/Libraries/Utilities/createStrictShapeTypeChecker.js b/Libraries/Utilities/createStrictShapeTypeChecker.js index 1c002950a..daaacb9b4 100644 --- a/Libraries/Utilities/createStrictShapeTypeChecker.js +++ b/Libraries/Utilities/createStrictShapeTypeChecker.js @@ -37,12 +37,14 @@ function createStrictShapeTypeChecker(shapeTypes) { var allKeys = merge(props[propName], shapeTypes); for (var key in allKeys) { var checker = shapeTypes[key]; - invariant( - checker, - `Invalid props.${propName} key \`${key}\` supplied to \`${componentName}\`.` + - `\nBad object: ` + JSON.stringify(props[propName], null, ' ') + - `\nValid keys: ` + JSON.stringify(Object.keys(shapeTypes), null, ' ') - ); + if (!checker) { + invariant( + false, + `Invalid props.${propName} key \`${key}\` supplied to \`${componentName}\`.` + + `\nBad object: ` + JSON.stringify(props[propName], null, ' ') + + `\nValid keys: ` + JSON.stringify(Object.keys(shapeTypes), null, ' ') + ); + } var error = checker(propValue, key, componentName, location); if (error) { invariant( diff --git a/ReactKit/Base/RCTBridge.h b/ReactKit/Base/RCTBridge.h index ff1c5d7ad..1cb53fc0a 100644 --- a/ReactKit/Base/RCTBridge.h +++ b/ReactKit/Base/RCTBridge.h @@ -33,10 +33,9 @@ static inline NSDictionary *RCTAPIErrorObject(NSString *msg) */ @interface RCTBridge : NSObject -- (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor - javaScriptModulesConfig:(NSDictionary *)javaScriptModulesConfig; +- (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor; -- (void)enqueueJSCall:(NSUInteger)moduleID methodID:(NSUInteger)methodID args:(NSArray *)args; +- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args; - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete; @property (nonatomic, readonly) RCTEventDispatcher *eventDispatcher; diff --git a/ReactKit/Base/RCTBridge.m b/ReactKit/Base/RCTBridge.m index 6ea5a037a..3604b9dff 100644 --- a/ReactKit/Base/RCTBridge.m +++ b/ReactKit/Base/RCTBridge.m @@ -8,7 +8,6 @@ #import "RCTInvalidating.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" -#import "RCTModuleIDs.h" #import "RCTUtils.h" /** @@ -36,19 +35,6 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; -static NSString *RCTModuleName(Class moduleClass) -{ - if ([moduleClass respondsToSelector:@selector(moduleName)]) { - - return [moduleClass moduleName]; - - } else { - - // Default implementation, works in most cases - return NSStringFromClass(moduleClass); - } -} - static NSDictionary *RCTNativeModuleClasses(void) { static NSMutableDictionary *modules; @@ -73,7 +59,7 @@ static NSDictionary *RCTNativeModuleClasses(void) } // Get module name - NSString *moduleName = RCTModuleName(cls); + NSString *moduleName = [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(cls); // Check module name is unique id existingClass = modules[moduleName]; @@ -90,23 +76,22 @@ static NSDictionary *RCTNativeModuleClasses(void) @implementation RCTBridge { NSMutableDictionary *_moduleInstances; - NSDictionary *_javaScriptModulesConfig; + NSMutableDictionary *_moduleIDLookup; + NSMutableDictionary *_methodIDLookup; id _javaScriptExecutor; } static id _latestJSExecutor; - (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor - javaScriptModulesConfig:(NSDictionary *)javaScriptModulesConfig { if ((self = [super init])) { _javaScriptExecutor = javaScriptExecutor; _latestJSExecutor = _javaScriptExecutor; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; - _javaScriptModulesConfig = javaScriptModulesConfig; _shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL); - // Register modules + // Instantiate modules _moduleInstances = [[NSMutableDictionary alloc] init]; [RCTNativeModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) { if (_moduleInstances[moduleName] == nil) { @@ -118,6 +103,8 @@ static id _latestJSExecutor; } }]; + _moduleIDLookup = [[NSMutableDictionary alloc] init]; + _methodIDLookup = [[NSMutableDictionary alloc] init]; [self doneRegisteringModules]; } @@ -168,10 +155,18 @@ static id _latestJSExecutor; /** * Like JS::call, for objective-c. */ -- (void)enqueueJSCall:(NSUInteger)moduleID methodID:(NSUInteger)methodID args:(NSArray *)args +- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args { - RCTAssertMainThread(); - [self _invokeRemoteJSModule:moduleID methodID:methodID args:args]; + NSNumber *moduleID = _moduleIDLookup[moduleDotMethod]; + RCTAssert(moduleID, @"Module '%@' not registered.", + [[moduleDotMethod componentsSeparatedByString:@"."] firstObject]); + + NSNumber *methodID = _methodIDLookup[moduleDotMethod]; + RCTAssert(methodID, @"Method '%@' not registered.", moduleDotMethod); + + [self _invokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, args]]; } - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete @@ -217,13 +212,6 @@ static id _latestJSExecutor; 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. */ @@ -333,21 +321,21 @@ static id _latestJSExecutor; // 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; + + NSMethodSignature *methodSignature = [target methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setArgument:&target atIndex:0]; [invocation setArgument:&selector atIndex:1]; // Retain used blocks until after invocation completes. - NSMutableArray *blocks = [NSMutableArray array]; - + NS_VALID_UNTIL_END_OF_SCOPE NSMutableArray *blocks = [NSMutableArray array]; + [params enumerateObjectsUsingBlock:^(id param, NSUInteger idx, BOOL *stop) { if ([param isEqual:[NSNull null]]) { param = nil; @@ -356,8 +344,58 @@ static id _latestJSExecutor; [blocks addObject:block]; param = block; } - - [invocation setArgument:¶m atIndex:idx + 2]; + + NSUInteger argIdx = idx + 2; + + BOOL shouldSet = YES; + const char *argumentType = [methodSignature getArgumentTypeAtIndex:argIdx]; + switch (argumentType[0]) { + case ':': + if ([param isKindOfClass:[NSString class]]) { + SEL selector = NSSelectorFromString(param); + [invocation setArgument:&selector atIndex:argIdx]; + shouldSet = NO; + } + break; + + case '*': + if ([param isKindOfClass:[NSString class]]) { + const char *string = [param UTF8String]; + [invocation setArgument:&string atIndex:argIdx]; + shouldSet = NO; + } + break; + +#define CASE(_value, _type, _selector) \ + case _value: \ + if ([param respondsToSelector:@selector(_selector)]) { \ + _type value = [param _selector]; \ + [invocation setArgument:&value atIndex:argIdx]; \ + shouldSet = NO; \ + } \ + break; + + CASE('c', char, charValue) + CASE('C', unsigned char, unsignedCharValue) + CASE('s', short, shortValue) + CASE('S', unsigned short, unsignedShortValue) + CASE('i', int, intValue) + CASE('I', unsigned int, unsignedIntValue) + CASE('l', long, longValue) + CASE('L', unsigned long, unsignedLongValue) + CASE('q', long long, longLongValue) + CASE('Q', unsigned long long, unsignedLongLongValue) + CASE('f', float, floatValue) + CASE('d', double, doubleValue) + CASE('B', BOOL, boolValue) + + default: + break; + } + + if (shouldSet) { + [invocation setArgument:¶m atIndex:argIdx]; + } }]; @try { @@ -366,10 +404,6 @@ static id _latestJSExecutor; @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; @@ -412,16 +446,71 @@ static id _latestJSExecutor; return invocation; } +- (NSArray *)JSMethods +{ + NSMutableArray *methods = [[NSMutableArray alloc] init]; + + // Add globally used methods + [methods addObjectsFromArray:@[ + @"Bundler.runApplication", + @"RCTEventEmitter.receiveEvent", + @"RCTEventEmitter.receiveTouches", + ]]; + + // NOTE: these methods are currently unused in the OSS project + // @"Dimensions.set", + // @"RCTDeviceEventEmitter.emit", + // @"RCTNativeAppEventEmitter.emit", + // @"ReactIOS.unmountComponentAtNodeAndRemoveContainer", + + // Register individual methods from modules + for (Class cls in RCTNativeModuleClasses().allValues) { + if (RCTClassOverridesClassMethod(cls, @selector(JSMethods))) { + [methods addObjectsFromArray:[cls JSMethods]]; + } + } + + return methods; +} + - (void)doneRegisteringModules { + // TODO: everything in this method can actually be determined + // statically from just the class, and can therefore be cached + // in a dispatch_once instead of being repeated every time we + // reload or create a new RootView. + 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: { ... }, ... }, ... } + /** + * This constructs the remote modules configuration data structure, + * which represents the native modules and methods that will be called + * by JS. A numeric ID is assigned to each module and method, which will + * be used to communicate via the bridge. The structure of each + * module is as follows: + * + * "ModuleName1": { + * "moduleID": 0, + * "methods": { + * "methodName1": { + * "methodID": 0, + * "type": "remote" + * }, + * "methodName2": { + * "methodID": 1, + * "type": "remote" + * }, + * etc... + * }, + * "constants": { + * ... + * } + * }, + * etc... + */ + NSUInteger moduleCount = RCTExportedMethodsByModule().count; - NSMutableDictionary *moduleConfigs = [NSMutableDictionary dictionaryWithCapacity:RCTExportedMethodsByModule().count]; + NSMutableDictionary *remoteModules = [NSMutableDictionary dictionaryWithCapacity:RCTExportedMethodsByModule().count]; for (NSUInteger i = 0; i < moduleCount; i++) { NSString *moduleName = RCTExportedModuleNameAtSortedIndex(i); NSArray *rawMethods = RCTExportedMethodsByModule()[moduleName]; @@ -433,35 +522,87 @@ static id _latestJSExecutor; }; }]; - NSMutableDictionary *moduleConfig = [NSMutableDictionary dictionary]; - moduleConfig[@"moduleID"] = @(i); - moduleConfig[@"methods"] = methods; + NSDictionary *module = @{ + @"moduleID": @(i), + @"methods": methods + }; - id target = [_moduleInstances objectForKey:moduleName]; - if ([target respondsToSelector:@selector(constantsToExport)]) { - moduleConfig[@"constants"] = [target constantsToExport]; + id target = _moduleInstances[moduleName]; + if (RCTClassOverridesClassMethod([target class], @selector(constantsToExport))) { + module = [module mutableCopy]; + ((NSMutableDictionary *)module)[@"constants"] = [[target class] constantsToExport]; } - moduleConfigs[moduleName] = moduleConfig; + remoteModules[moduleName] = module; } - NSDictionary *batchedBridgeConfig = @{ - @"remoteModuleConfig": moduleConfigs, - @"localModulesConfig": _javaScriptModulesConfig - }; - - NSString *configJSON = RCTJSONStringify(batchedBridgeConfig, NULL); - objectsToInject[@"__fbBatchedBridgeConfig"] = configJSON; - + + /** + * As above, but for local modules/methods, which represent JS classes + * and methods that will be called by the native code via the bridge. + * Structure is essentially the same as for remote modules: + * + * "ModuleName1": { + * "moduleID": 0, + * "methods": { + * "methodName1": { + * "methodID": 0, + * "type": "local" + * }, + * "methodName2": { + * "methodID": 1, + * "type": "local" + * }, + * etc... + * } + * }, + * etc... + */ + + NSMutableDictionary *localModules = [[NSMutableDictionary alloc] init]; + for (NSString *moduleDotMethod in [self JSMethods]) { + + NSArray *parts = [moduleDotMethod componentsSeparatedByString:@"."]; + RCTAssert(parts.count == 2, @"'%@' is not a valid JS method definition - expected 'Module.method' format.", moduleDotMethod); + + // Add module if it doesn't already exist + NSString *moduleName = parts[0]; + NSDictionary *module = localModules[moduleName]; + if (!module) { + module = @{ + @"moduleID": @(localModules.count), + @"methods": [[NSMutableDictionary alloc] init] + }; + localModules[moduleName] = module; + } + + // Add method if it doesn't already exist + NSString *methodName = parts[1]; + NSMutableDictionary *methods = module[@"methods"]; + if (!methods[methodName]) { + methods[methodName] = @{ + @"methodID": @(methods.count), + @"type": @"local" + }; + } + + // Add module and method lookup + _moduleIDLookup[moduleDotMethod] = module[@"moduleID"]; + _methodIDLookup[moduleDotMethod] = methods[methodName][@"methodID"]; + } + + /** + * Inject module data into JS context + */ + NSString *configJSON = RCTJSONStringify(@{ + @"remoteModuleConfig": remoteModules, + @"localModulesConfig": localModules + }, NULL); 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); - }]; + [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" 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"); - } + if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) { + RCTLogMustFix(@"JavaScriptExecutor take too long to inject JSON object"); } } diff --git a/ReactKit/Base/RCTConvert.h b/ReactKit/Base/RCTConvert.h index f22d395ef..efa35674e 100644 --- a/ReactKit/Base/RCTConvert.h +++ b/ReactKit/Base/RCTConvert.h @@ -41,6 +41,7 @@ + (UIColor *)UIColor:(id)json; + (CGColorRef)CGColor:(id)json; ++ (CAKeyframeAnimation *)GIF:(id)json; + (UIImage *)UIImage:(id)json; + (CGImageRef)CGImage:(id)json; @@ -56,4 +57,4 @@ + (css_position_type_t)css_position_type_t:(id)json; + (css_wrap_type_t)css_wrap_type_t:(id)json; -@end \ No newline at end of file +@end diff --git a/ReactKit/Base/RCTConvert.m b/ReactKit/Base/RCTConvert.m index 63673a678..cd91d0e6c 100644 --- a/ReactKit/Base/RCTConvert.m +++ b/ReactKit/Base/RCTConvert.m @@ -2,6 +2,9 @@ #import "RCTConvert.h" +#import +#import + #import "RCTLog.h" CGFloat const RCTDefaultFontSize = 14; @@ -426,6 +429,83 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] return [self UIColor:json].CGColor; } ++ (CAKeyframeAnimation *)GIF:(id)json +{ + CGImageSourceRef imageSource; + if ([json isKindOfClass:[NSString class]]) { + NSString *path = json; + if (path.length == 0) { + return nil; + } + + NSURL *fileURL = [path isAbsolutePath] ? [NSURL fileURLWithPath:path] : [[NSBundle mainBundle] URLForResource:path withExtension:nil]; + imageSource = CGImageSourceCreateWithURL((CFURLRef)fileURL, NULL); + } else if ([json isKindOfClass:[NSData class]]) { + NSData *data = json; + if (data.length == 0) { + return nil; + } + + imageSource = CGImageSourceCreateWithData((CFDataRef)data, NULL); + } + + if (!UTTypeConformsTo(CGImageSourceGetType(imageSource), kUTTypeGIF)) { + CFRelease(imageSource); + return nil; + } + + NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL); + NSUInteger loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; + + size_t imageCount = CGImageSourceGetCount(imageSource); + NSTimeInterval duration = 0; + NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount]; + NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount]; + for (size_t i = 0; i < imageCount; i++) { + CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); + NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL); + NSDictionary *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary]; + + const NSTimeInterval kDelayTimeIntervalDefault = 0.1; + NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime]; + if (delayTime == nil) { + if (i == 0) { + delayTime = @(kDelayTimeIntervalDefault); + } else { + delayTime = delays[i - 1]; + } + } + + const NSTimeInterval kDelayTimeIntervalMinimum = 0.02; + if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) { + delayTime = @(kDelayTimeIntervalDefault); + } + + duration += delayTime.doubleValue; + delays[i] = delayTime; + images[i] = (__bridge_transfer id)image; + } + + CFRelease(imageSource); + + NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count]; + NSTimeInterval runningDuration = 0; + for (NSNumber *delayNumber in delays) { + [keyTimes addObject:@(runningDuration / duration)]; + runningDuration += delayNumber.doubleValue; + } + + [keyTimes addObject:@1.0]; + + CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"]; + animation.calculationMode = kCAAnimationDiscrete; + animation.repeatCount = loopCount == 0 ? HUGE_VALF : loopCount; + animation.keyTimes = keyTimes; + animation.values = images; + animation.duration = duration; + return animation; +} + + (UIImage *)UIImage:(id)json { if (![json isKindOfClass:[NSString class]]) { diff --git a/ReactKit/Base/RCTEventDispatcher.h b/ReactKit/Base/RCTEventDispatcher.h index 3f9e8b2a3..d06994959 100644 --- a/ReactKit/Base/RCTEventDispatcher.h +++ b/ReactKit/Base/RCTEventDispatcher.h @@ -33,9 +33,11 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { - (instancetype)initWithBridge:(RCTBridge *)bridge; /** - * Send an arbitrary event type + * Send a named event. For most purposes, use the an + * event type of RCTEventTypeDefault, the other types + * are used internally by the React framework. */ -- (void)sendRawEventWithType:(NSString *)eventType body:(NSDictionary *)body; +- (void)sendEventWithName:(NSString *)name body:(NSDictionary *)body; /** * Send an array of touch events diff --git a/ReactKit/Base/RCTEventDispatcher.m b/ReactKit/Base/RCTEventDispatcher.m index a7484fab0..fec3e32ae 100644 --- a/ReactKit/Base/RCTEventDispatcher.m +++ b/ReactKit/Base/RCTEventDispatcher.m @@ -3,7 +3,6 @@ #import "RCTEventDispatcher.h" #import "RCTBridge.h" -#import "RCTModuleIDs.h" #import "UIView+ReactKit.h" @implementation RCTEventDispatcher @@ -19,39 +18,13 @@ return self; } -- (NSArray *)touchEvents +- (void)sendEventWithName:(NSString *)name body:(NSDictionary *)body { - static NSArray *events; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - events = @[ - @"topTouchStart", - @"topTouchMove", - @"topTouchEnd", - @"topTouchCancel", - ]; - }); - - return events; -} - -- (void)sendRawEventWithType:(NSString *)eventType body:(NSDictionary *)body -{ - static NSSet *touchEvents; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - touchEvents = [NSSet setWithArray:[self touchEvents]]; - }); - - RCTAssert(![touchEvents containsObject:eventType], @"Touch events must be" - "sent via the sendTouchEventWithOrderedTouches: method, not sendRawEventWithType:"); - RCTAssert([body[@"target"] isKindOfClass:[NSNumber class]], - @"Event body dictionary must include a 'target' property containing a react tag"); + @"Event body dictionary must include a 'target' property containing a react tag"); - [_bridge enqueueJSCall:RCTModuleIDReactIOSEventEmitter - methodID:RCTEventEmitterReceiveEvent - args:@[body[@"target"], eventType, body]]; + [_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent" + args:@[body[@"target"], name, body]]; } /** @@ -69,30 +42,32 @@ touches:(NSArray *)touches changedIndexes:(NSArray *)changedIndexes { + static NSString *events[] = { + @"topTouchStart", + @"topTouchMove", + @"topTouchEnd", + @"topTouchCancel", + }; + RCTAssert(touches.count, @"No touches in touchEventArgsForOrderedTouches"); - [_bridge enqueueJSCall:RCTModuleIDReactIOSEventEmitter - methodID:RCTEventEmitterReceiveTouches - args:@[[self touchEvents][type], touches, changedIndexes]]; + [_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches" + args:@[events[type], touches, changedIndexes]]; } - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag text:(NSString *)text { - static NSArray *events; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - events = @[ - @"topFocus", - @"topBlur", - @"topChange", - @"topSubmitEditing", - @"topEndEditing", - ]; - }); + static NSString *events[] = { + @"topFocus", + @"topBlur", + @"topChange", + @"topSubmitEditing", + @"topEndEditing", + }; - [self sendRawEventWithType:events[type] body:@{ + [self sendEventWithName:events[type] body:@{ @"text": text, @"target": reactTag }]; @@ -111,18 +86,14 @@ scrollView:(UIScrollView *)scrollView userData:(NSDictionary *)userData { - static NSArray *events; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - events = @[ - @"topScrollBeginDrag", - @"topScroll", - @"topScrollEndDrag", - @"topMomentumScrollBegin", - @"topMomentumScrollEnd", - @"topScrollAnimationEnd", - ]; - }); + static NSString *events[] = { + @"topScrollBeginDrag", + @"topScroll", + @"topScrollEndDrag", + @"topMomentumScrollBegin", + @"topMomentumScrollEnd", + @"topScrollAnimationEnd", + }; NSDictionary *body = @{ @"contentOffset": @{ @@ -147,7 +118,7 @@ body = mutableBody; } - [self sendRawEventWithType:events[type] body:body]; + [self sendEventWithName:events[type] body:body]; } @end diff --git a/ReactKit/Base/RCTExport.h b/ReactKit/Base/RCTExport.h index f78370b9c..d4bd1772b 100644 --- a/ReactKit/Base/RCTExport.h +++ b/ReactKit/Base/RCTExport.h @@ -26,6 +26,7 @@ extern NSString *RCTExportedModuleNameAtSortedIndex(NSUInteger index); extern NSDictionary *RCTExportedMethodsByModule(void); extern BOOL RCTSetProperty(id target, NSString *keypath, id value); +extern BOOL RCTCopyProperty(id target, id source, NSString *keypath); extern BOOL RCTCallSetter(id target, SEL setter, id value); /* ------------------------------------------------------------------- */ @@ -66,9 +67,22 @@ _RCTExportSectionName))) static const RCTExportEntry __rct_export_entry__ = { __ /** * Injects constants into JS. These constants are made accessible via - * NativeModules.moduleName.X. + * NativeModules.moduleName.X. Note that this method is not inherited when you + * subclass a module, and you should not call [super constantsToExport] when + * implementing it. */ -- (NSDictionary *)constantsToExport; ++ (NSDictionary *)constantsToExport; + +/** + * An array of JavaScript methods that the module will call via the + * -[RCTBridge enqueueJSCall:args:] method. Each method should be specified + * as a string of the form "JSModuleName.jsMethodName". Attempting to call a + * method that has not been registered will result in an error. If a method + * has already been regsistered by another module, it is not necessary to + * register it again, but it is good pratice. Registering the same method + * more than once is silently ignored and will not result in an error. + */ ++ (NSArray *)JSMethods; /** * Notifies the module that a batch of JS method invocations has just completed. @@ -84,9 +98,18 @@ _RCTExportSectionName))) static const RCTExportEntry __rct_export_entry__ = { __ @protocol RCTNativeViewModule /** - * This method instantiates a native view to be managed by the module. + * Designated initializer for view modules. The event dispatched can either be + * used directly by the module, or passed on to instantiated views for event handling. */ -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; + +/** + * This method instantiates a native view to be managed by the module. The method + * will be called many times, and should return a fresh instance each time. The + * view module MUST NOT cache the returned view and return the same instance + * for subsequent calls. + */ +- (UIView *)view; @optional @@ -98,7 +121,8 @@ _RCTExportSectionName))) static const RCTExportEntry __rct_export_entry__ = { __ /** * This method instantiates a shadow view to be managed by the module. If omitted, - * an ordinary RCTShadowView instance will be created. + * an ordinary RCTShadowView instance will be created. As with the -view method, + * the -shadowView method should return a fresh instance each time it is called. */ - (RCTShadowView *)shadowView; @@ -164,8 +188,11 @@ RCT_REMAP_VIEW_PROPERTY(name, name) * } * } * } + * + * Note that this method is not inherited when you subclass a view module, and + * you should not call [super customBubblingEventTypes] when implementing it. */ -- (NSDictionary *)customBubblingEventTypes; ++ (NSDictionary *)customBubblingEventTypes; /** * Returns a dictionary of config data passed to JS that defines eligible events @@ -177,14 +204,19 @@ RCT_REMAP_VIEW_PROPERTY(name, name) * @"registrationName": @"onTwirl" * } * } + * + * Note that this method is not inherited when you subclass a view module, and + * you should not call [super customDirectEventTypes] when implementing it. */ -- (NSDictionary *)customDirectEventTypes; ++ (NSDictionary *)customDirectEventTypes; /** * Injects constants into JS. These constants are made accessible via - * NativeModules.moduleName.X. + * NativeModules.moduleName.X. Note that this method is not inherited when you + * subclass a view module, and you should not call [super constantsToExport] + * when implementing it. */ -- (NSDictionary *)constantsToExport; ++ (NSDictionary *)constantsToExport; /** * To deprecate, hopefully diff --git a/ReactKit/Base/RCTExport.m b/ReactKit/Base/RCTExport.m index 23e6b35f4..be206cba4 100644 --- a/ReactKit/Base/RCTExport.m +++ b/ReactKit/Base/RCTExport.m @@ -349,6 +349,36 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) return YES; } +BOOL RCTCopyProperty(id target, id source, NSString *keypath) +{ + // Split keypath + NSArray *parts = [keypath componentsSeparatedByString:@"."]; + NSString *key = [parts lastObject]; + for (NSUInteger i = 0; i < parts.count - 1; i++) { + source = [source valueForKey:parts[i]]; + target = [target valueForKey:parts[i]]; + if (!source || !target) { + return NO; + } + } + + // Check class for property definition + if (!class_getProperty([source class], [key UTF8String])) { + // Check if setter exists + SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", + [[key substringToIndex:1] uppercaseString], + [key substringFromIndex:1]]); + + if (![source respondsToSelector:setter] + || ![target respondsToSelector:setter]) { + return NO; + } + } + + [target setValue:[source valueForKey:key] forKey:key]; + return YES; +} + BOOL RCTCallSetter(id target, SEL setter, id value) { // Get property name diff --git a/ReactKit/Base/RCTImageDownloader.h b/ReactKit/Base/RCTImageDownloader.h index 241846cdd..b525ea1a7 100644 --- a/ReactKit/Base/RCTImageDownloader.h +++ b/ReactKit/Base/RCTImageDownloader.h @@ -2,12 +2,16 @@ #import +typedef void (^RCTDataDownloadBlock)(NSData *data, NSError *error); typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error); @interface RCTImageDownloader : NSObject + (instancetype)sharedInstance; +- (id)downloadDataForURL:(NSURL *)url + block:(RCTDataDownloadBlock)block; + - (id)downloadImageForURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale diff --git a/ReactKit/Base/RCTImageDownloader.m b/ReactKit/Base/RCTImageDownloader.m index f3f996fbe..984e6cf6b 100644 --- a/ReactKit/Base/RCTImageDownloader.m +++ b/ReactKit/Base/RCTImageDownloader.m @@ -18,6 +18,20 @@ return sharedInstance; } +- (id)downloadDataForURL:(NSURL *)url + block:(RCTDataDownloadBlock)block +{ + NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + // Dispatch back to main thread + dispatch_async(dispatch_get_main_queue(), ^{ + block(data, error); + }); + }]; + + [task resume]; + return task; +} + - (id)downloadImageForURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale diff --git a/ReactKit/Base/RCTModuleIDs.h b/ReactKit/Base/RCTModuleIDs.h deleted file mode 100644 index f03f69002..000000000 --- a/ReactKit/Base/RCTModuleIDs.h +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#import - -/** - * 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, -}; - -/** - * JS module `RCTIOSEventEmitter`. - */ -typedef NS_ENUM(NSUInteger, RCTEventEmitterRemoteMethodIDs) { - RCTEventEmitterReceiveEvent = 0, - RCTEventEmitterReceiveTouches -}; - -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, RCTDeviceEventEmitterMethodIDs) { - RCTDeviceEventEmitterEmit = 0 -}; - -@interface RCTModuleIDs : NSObject - -+ (NSDictionary *)config; - -@end - diff --git a/ReactKit/Base/RCTModuleIDs.m b/ReactKit/Base/RCTModuleIDs.m deleted file mode 100644 index e83707906..000000000 --- a/ReactKit/Base/RCTModuleIDs.m +++ /dev/null @@ -1,92 +0,0 @@ -// 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" - }, - } - }, - - @"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 - diff --git a/ReactKit/Base/RCTRootView.m b/ReactKit/Base/RCTRootView.m index a5e9b56f1..a6ec65038 100644 --- a/ReactKit/Base/RCTRootView.m +++ b/ReactKit/Base/RCTRootView.m @@ -6,11 +6,11 @@ #import "RCTContextExecutor.h" #import "RCTEventDispatcher.h" #import "RCTJavaScriptAppEngine.h" -#import "RCTModuleIDs.h" #import "RCTTouchHandler.h" #import "RCTUIManager.h" #import "RCTUtils.h" #import "RCTViewManager.h" +#import "RCTWebViewExecutor.h" #import "UIView+ReactKit.h" #import "RCTKeyCommands.h" @@ -23,6 +23,8 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" RCTTouchHandler *_touchHandler; } +static BOOL _useWebExec; + + (void)initialize { @@ -34,6 +36,13 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" action:^(UIKeyCommand *command) { [self reloadAll]; }]; + // Cmd-D reloads using the web view executor, allows attaching from Safari dev tools. + [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"d" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + _useWebExec = YES; + [self reloadAll]; + }]; #endif @@ -98,8 +107,8 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" @"rootTag": self.reactTag ?: @0, @"initialProps": self.initialProperties ?: @{}, }; - [_bridge enqueueJSCall:RCTModuleIDBundler - methodID:RCTBundlerRunApplication + + [_bridge enqueueJSCall:@"Bundler.runApplication" args:@[moduleName, appParameters]]; } } @@ -123,9 +132,13 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" [_executor invalidate]; [_bridge invalidate]; - _executor = [[RCTContextExecutor alloc] init]; - _bridge = [[RCTBridge alloc] initWithJavaScriptExecutor:_executor - javaScriptModulesConfig:[RCTModuleIDs config]]; + if (!_useWebExec) { + _executor = [[RCTContextExecutor alloc] init]; + } else { + _executor = [[RCTWebViewExecutor alloc] init]; + } + + _bridge = [[RCTBridge alloc] initWithJavaScriptExecutor:_executor]; _appEngine = [[RCTJavaScriptAppEngine alloc] initWithBridge:_bridge]; _touchHandler = [[RCTTouchHandler alloc] initWithEventDispatcher:_bridge.eventDispatcher rootView:self]; @@ -165,22 +178,4 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" [[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 diff --git a/ReactKit/Base/RCTSparseArray.h b/ReactKit/Base/RCTSparseArray.h index 4dd7bceba..e7a999a08 100644 --- a/ReactKit/Base/RCTSparseArray.h +++ b/ReactKit/Base/RCTSparseArray.h @@ -4,9 +4,8 @@ @interface RCTSparseArray : NSObject -- (instancetype)init; -- (instancetype)initWithCapacity:(NSUInteger)capacity; -- (instancetype)initWithSparseArray:(RCTSparseArray *)sparseArray; +- (instancetype)initWithCapacity:(NSUInteger)capacity NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithSparseArray:(RCTSparseArray *)sparseArray NS_DESIGNATED_INITIALIZER; + (instancetype)sparseArray; + (instancetype)sparseArrayWithCapacity:(NSUInteger)capacity; diff --git a/ReactKit/Base/RCTSparseArray.m b/ReactKit/Base/RCTSparseArray.m index ca88d2db6..12c7386cd 100644 --- a/ReactKit/Base/RCTSparseArray.m +++ b/ReactKit/Base/RCTSparseArray.m @@ -28,14 +28,6 @@ return self; } -- (instancetype)initWithStorage:(NSDictionary *)storage -{ - if ((self = [super init])) { - _storage = [storage copy]; - } - return self; -} - + (instancetype)sparseArray { return [[self alloc] init]; diff --git a/ReactKit/Base/RCTTouchHandler.m b/ReactKit/Base/RCTTouchHandler.m index 09071bc3e..8b4c43e7c 100644 --- a/ReactKit/Base/RCTTouchHandler.m +++ b/ReactKit/Base/RCTTouchHandler.m @@ -11,6 +11,9 @@ #import "RCTUtils.h" #import "UIView+ReactKit.h" +// TODO: this class behaves a lot like a module, and could be implemented as a +// module if we were to assume that modules and RootViews had a 1:1 relationship + @implementation RCTTouchHandler { __weak UIView *_rootView; @@ -27,11 +30,6 @@ NSMutableArray *_touchViews; } -- (instancetype)init -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - - (instancetype)initWithTarget:(id)target action:(SEL)action { RCT_NOT_DESIGNATED_INITIALIZER(); diff --git a/ReactKit/Base/RCTUtils.h b/ReactKit/Base/RCTUtils.h index 35bb43f56..3011d5beb 100644 --- a/ReactKit/Base/RCTUtils.h +++ b/ReactKit/Base/RCTUtils.h @@ -20,8 +20,9 @@ id RCTJSONParse(NSString *jsonString, NSError **error); // Get MD5 hash of a string NSString *RCTMD5Hash(NSString *string); -// Get screen scale in a thread-safe way +// Get screen metrics in a thread-safe way CGFloat RCTScreenScale(void); +CGSize RCTScreenSize(void); // Round float coordinates to nearest whole screen pixel (not point) CGFloat RCTRoundPixelValue(CGFloat value); @@ -34,3 +35,7 @@ NSTimeInterval RCTTGetAbsoluteTime(void); // Method swizzling void RCTSwapClassMethods(Class cls, SEL original, SEL replacement); void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement); + +// Module subclass support +BOOL RCTClassOverridesClassMethod(Class cls, SEL selector); +BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector); diff --git a/ReactKit/Base/RCTUtils.m b/ReactKit/Base/RCTUtils.m index a6c241207..615c5235e 100644 --- a/ReactKit/Base/RCTUtils.m +++ b/ReactKit/Base/RCTUtils.m @@ -35,7 +35,7 @@ NSString *RCTMD5Hash(NSString *string) CGFloat RCTScreenScale() { - static CGFloat scale = -1; + static CGFloat scale; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ if (![NSThread isMainThread]) { @@ -50,6 +50,23 @@ CGFloat RCTScreenScale() return scale; } +CGSize RCTScreenSize() +{ + static CGSize size; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (![NSThread isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + size = [UIScreen mainScreen].bounds.size; + }); + } else { + size = [UIScreen mainScreen].bounds.size; + } + }); + + return size; +} + CGFloat RCTRoundPixelValue(CGFloat value) { CGFloat scale = RCTScreenScale(); @@ -119,4 +136,25 @@ void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement) { method_exchangeImplementations(originalMethod, replacementMethod); } -} \ No newline at end of file +} + +BOOL RCTClassOverridesClassMethod(Class cls, SEL selector) +{ + return RCTClassOverridesInstanceMethod(object_getClass(cls), selector); +} + +BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector) +{ + unsigned int numberOfMethods; + Method *methods = class_copyMethodList(cls, &numberOfMethods); + for (unsigned int i = 0; i < numberOfMethods; i++) + { + if (method_getName(methods[i]) == selector) + { + free(methods); + return YES; + } + } + return NO; +} + diff --git a/ReactKit/Executors/RCTWebViewExecutor.m b/ReactKit/Executors/RCTWebViewExecutor.m index 307661799..e774089bd 100644 --- a/ReactKit/Executors/RCTWebViewExecutor.m +++ b/ReactKit/Executors/RCTWebViewExecutor.m @@ -41,6 +41,11 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) return self; } +- (id)init +{ + return [self initWithWebView:[[UIWebView alloc] init]]; +} + - (BOOL)isValid { return _webView != nil; @@ -98,7 +103,13 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) sourceURL:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete { - RCTAssertMainThread(); + if (![NSThread isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [self executeApplicationScript:script sourceURL:url onComplete:onComplete]; + }); + return; + } + RCTAssert(onComplete != nil, @""); _onApplicationScriptLoaded = onComplete; @@ -137,26 +148,12 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) */ - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block { - if (![NSThread isMainThread]) { - [self performSelectorOnMainThread:@selector(executeBlockOnJavaScriptQueue:) - withObject:block - waitUntilDone:YES]; - } else { - [self performSelector:@selector(_onMainThreadExecuteBlockAfterDelay:) - withObject:block afterDelay:0.001 // This can't be zero! - inModes:@[NSDefaultRunLoopMode, UITrackingRunLoopMode]]; - } -} + dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC); -/** - * This timing delay is needed to avoid crashes in WebKit when setting a - * breakpoint or `debugger` statement and debugging via the remote Safari - * inspector. - */ -- (void)_onMainThreadExecuteBlockAfterDelay:(dispatch_block_t)block -{ - RCTAssertMainThread(); - block(); + dispatch_after(when, dispatch_get_main_queue(), ^{ + RCTAssertMainThread(); + block(); + }); } /** diff --git a/ReactKit/Modules/RCTTiming.m b/ReactKit/Modules/RCTTiming.m index cc45fdbc2..d01b07509 100644 --- a/ReactKit/Modules/RCTTiming.m +++ b/ReactKit/Modules/RCTTiming.m @@ -4,7 +4,6 @@ #import "RCTBridge.h" #import "RCTLog.h" -#import "RCTModuleIDs.h" #import "RCTSparseArray.h" #import "RCTUtils.h" @@ -55,6 +54,11 @@ id _updateTimer; } ++ (NSArray *)JSMethods +{ + return @[@"RCTJSTimers.callTimers"]; +} + - (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super init])) { @@ -143,7 +147,7 @@ // call timers that need to be called if ([timersToCall count] > 0) { - [_bridge enqueueJSCall:RCTModuleIDJSTimers methodID:RCTJSTimersCallTimers args:@[timersToCall]]; + [_bridge enqueueJSCall:@"RCTJSTimers.callTimers" args:@[timersToCall]]; } } @@ -155,14 +159,14 @@ * Date.now() from JS and then subtracting that from the current time here. */ - (void)createTimer:(NSNumber *)callbackID - duration:(NSNumber *)jsDuration - jsSchedulingTime:(NSNumber *)jsSchedulingTime - repeats:(NSNumber *)repeats + duration:(double)jsDuration + jsSchedulingTime:(double)jsSchedulingTime + repeats:(BOOL)repeats { RCT_EXPORT(); - NSTimeInterval interval = jsDuration.doubleValue / 1000; - NSTimeInterval jsCreationTimeSinceUnixEpoch = jsSchedulingTime.doubleValue / 1000; + NSTimeInterval interval = jsDuration / 1000; + NSTimeInterval jsCreationTimeSinceUnixEpoch = jsSchedulingTime / 1000; NSTimeInterval currentTimeSinceUnixEpoch = [[NSDate date] timeIntervalSince1970]; NSTimeInterval jsSchedulingOverhead = currentTimeSinceUnixEpoch - jsCreationTimeSinceUnixEpoch; if (jsSchedulingOverhead < 0) { diff --git a/ReactKit/Modules/RCTUIManager.h b/ReactKit/Modules/RCTUIManager.h index 9547e096d..3b13e69f1 100644 --- a/ReactKit/Modules/RCTUIManager.h +++ b/ReactKit/Modules/RCTUIManager.h @@ -12,7 +12,7 @@ @protocol RCTScrollableProtocol; @protocol RCTViewNodeProtocol; -@interface RCTUIManager : NSObject +@interface RCTUIManager : NSObject - (instancetype)initWithBridge:(RCTBridge *)bridge; diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index d12c8f998..75d58a65d 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -34,26 +34,6 @@ static void RCTTraverseViewNodes(id view, react_view_node_b } } -static NSString *RCTModuleName(Class moduleClass) -{ - if ([moduleClass respondsToSelector:@selector(moduleName)]) { - - return [moduleClass moduleName]; - - } else { - - // Default implementation, works in most cases - NSString *className = NSStringFromClass(moduleClass); - if ([className hasPrefix:@"RCTUI"]) { - className = [className substringFromIndex:@"RCT".length]; - } - if ([className hasSuffix:@"Manager"]) { - className = [className substringToIndex:className.length - @"Manager".length]; - } - return className; - } -} - static NSDictionary *RCTViewModuleClasses(void) { static NSMutableDictionary *modules; @@ -78,7 +58,7 @@ static NSDictionary *RCTViewModuleClasses(void) } // Get module name - NSString *moduleName = RCTModuleName(cls); + NSString *moduleName = [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(cls); // Check module name is unique id existingClass = modules[moduleName]; @@ -131,7 +111,7 @@ static NSDictionary *RCTViewModuleClasses(void) // Instantiate view managers NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) { - viewManagers[moduleName] = [[moduleClass alloc] init]; + viewManagers[moduleName] = [[moduleClass alloc] initWithEventDispatcher:_bridge.eventDispatcher]; }]; _viewManagers = viewManagers; @@ -535,11 +515,6 @@ static NSDictionary *RCTViewModuleClasses(void) } } -- (UIView *)viewForViewManager:(id )manager -{ - return [manager viewWithEventDispatcher:_bridge.eventDispatcher]; -} - static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, id manager) { // TODO: cache respondsToSelector tests @@ -581,7 +556,9 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView if (obj == [NSNull null]) { // Copy property from default view to current - RCTSetProperty(shadowView, key, [defaultView valueForKey:key]); + // Note: not just doing `[defaultView valueForKey:key]`, the + // key may not exist, in which case we'd get an exception. + RCTCopyProperty(shadowView, defaultView, key); } else { RCTSetProperty(shadowView, key, obj); } @@ -619,10 +596,10 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView // Note the default is setup after the props are read for the first time ever // for this className - this is ok because we only use the default for restoring // defaults, which never happens on first creation. - uiManager->_defaultViews[moduleName] = [uiManager viewForViewManager:manager]; + uiManager->_defaultViews[moduleName] = [manager view]; } - UIView *view = [uiManager viewForViewManager:manager]; + UIView *view = [manager view]; if (view) { // Set required properties view.reactTag = reactTag; @@ -1013,7 +990,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }]; } -- (NSDictionary *)allBubblingEventTypesConfigs ++ (NSDictionary *)allBubblingEventTypesConfigs { NSMutableDictionary *customBubblingEventTypesConfigs = [@{ // Bubble dispatched events @@ -1103,20 +1080,20 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }, } mutableCopy]; - for (id viewManager in _viewManagers.allValues) { - NSDictionary *bubblingEvents = [viewManager respondsToSelector:@selector(customBubblingEventTypes)]? [viewManager customBubblingEventTypes] : nil; - if (bubblingEvents) { - for (NSString *eventName in bubblingEvents) { - RCTCAssert(!customBubblingEventTypesConfigs[eventName], @"Event %@ registered multiple times.", eventName); + [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { + if (RCTClassOverridesClassMethod(cls, @selector(customBubblingEventTypes))) { + NSDictionary *eventTypes = [cls customBubblingEventTypes]; + for (NSString *eventName in eventTypes) { + RCTCAssert(!customBubblingEventTypesConfigs[eventName], @"Event '%@' registered multiple times.", eventName); } - [customBubblingEventTypesConfigs addEntriesFromDictionary:bubblingEvents]; + [customBubblingEventTypesConfigs addEntriesFromDictionary:eventTypes]; } - } + }]; return customBubblingEventTypesConfigs; } -- (NSDictionary *)allDirectEventTypesConfigs ++ (NSDictionary *)allDirectEventTypesConfigs { NSMutableDictionary *customDirectEventTypes = [@{ @"topScrollBeginDrag": @{ @@ -1154,22 +1131,21 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }, } mutableCopy]; - for (id viewManager in _viewManagers.allValues) { - NSDictionary *bubblingEvents = [viewManager respondsToSelector:@selector(customDirectEventTypes)] ? [viewManager customDirectEventTypes] : nil; - if (bubblingEvents) { - for (NSString *eventName in bubblingEvents) { - RCTCAssert(!customDirectEventTypes[eventName], @"Event %@ registered multiple times.", eventName); + [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { + if (RCTClassOverridesClassMethod(cls, @selector(customDirectEventTypes))) { + NSDictionary *eventTypes = [cls customDirectEventTypes]; + for (NSString *eventName in eventTypes) { + RCTCAssert(!customDirectEventTypes[eventName], @"Event '%@' registered multiple times.", eventName); } - [customDirectEventTypes addEntriesFromDictionary:bubblingEvents]; + [customDirectEventTypes addEntriesFromDictionary:eventTypes]; } - } + }]; return customDirectEventTypes; } -- (NSDictionary *)constantsToExport ++ (NSDictionary *)constantsToExport { - UIScreen *screen = [UIScreen mainScreen]; NSMutableDictionary *allJSConstants = [@{ @"customBubblingEventTypes": [self allBubblingEventTypesConfigs], @"customDirectEventTypes": [self allDirectEventTypesConfigs], @@ -1180,13 +1156,13 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }, @"Dimensions": @{ @"window": @{ - @"width": @(screen.bounds.size.width), - @"height": @(screen.bounds.size.height), - @"scale": @(screen.scale), + @"width": @(RCTScreenSize().width), + @"height": @(RCTScreenSize().height), + @"scale": @(RCTScreenScale()), }, @"modalFullscreenView": @{ - @"width": @(screen.bounds.size.width), - @"height": @(screen.bounds.size.height), + @"width": @(RCTScreenSize().width), + @"height": @(RCTScreenSize().width), }, }, @"StyleConstants": @{ @@ -1224,12 +1200,15 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }, } mutableCopy]; - [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, id viewManager, BOOL *stop) { - NSDictionary *constants = [viewManager respondsToSelector:@selector(constantsToExport)] ? [viewManager constantsToExport] : nil; - if (constants) { - RCTAssert(allJSConstants[name] == nil , @"Cannot redefine constant namespace: %@", name); + [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { + // TODO: should these be inherited? + NSDictionary *constants = RCTClassOverridesClassMethod(cls, @selector(constantsToExport)) ? [cls constantsToExport] : nil; + if ([constants count]) { + NSMutableDictionary *namespace = [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; + RCTAssert(namespace[@"Constants"] == nil , @"Cannot redefine Constants in namespace: %@", name); // add an additional 'Constants' namespace for each class - allJSConstants[name] = @{@"Constants": constants}; + namespace[@"Constants"] = constants; + allJSConstants[name] = [namespace copy]; } }]; diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index 5d2bbc692..036ef258a 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -50,7 +50,6 @@ 83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA5F1A601EAA00E9B192 /* RCTBridge.m */; }; 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */; }; 83CBBA871A60202500E9B192 /* RCTJavaScriptAppEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA861A60202500E9B192 /* RCTJavaScriptAppEngine.m */; }; - 83CBBA8B1A60204600E9B192 /* RCTModuleIDs.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA8A1A60204600E9B192 /* RCTModuleIDs.m */; }; 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */; }; 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBACB1A6023D300E9B192 /* RCTConvert.m */; }; 83EEC2EE1A604AB200C39218 /* RCTModuleMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 83EEC2ED1A604AB200C39218 /* RCTModuleMethod.m */; }; @@ -162,8 +161,6 @@ 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTEventDispatcher.m; sourceTree = ""; }; 83CBBA851A60202500E9B192 /* RCTJavaScriptAppEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptAppEngine.h; sourceTree = ""; }; 83CBBA861A60202500E9B192 /* RCTJavaScriptAppEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptAppEngine.m; sourceTree = ""; }; - 83CBBA891A60204600E9B192 /* RCTModuleIDs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModuleIDs.h; sourceTree = ""; }; - 83CBBA8A1A60204600E9B192 /* RCTModuleIDs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleIDs.m; sourceTree = ""; }; 83CBBA961A6020BB00E9B192 /* RCTTouchHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTouchHandler.h; sourceTree = ""; }; 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTouchHandler.m; sourceTree = ""; }; 83CBBACA1A6023D300E9B192 /* RCTConvert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTConvert.h; sourceTree = ""; }; @@ -329,8 +326,6 @@ 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */, 83CBBA4D1A601E3B00E9B192 /* RCTLog.h */, 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */, - 83CBBA891A60204600E9B192 /* RCTModuleIDs.h */, - 83CBBA8A1A60204600E9B192 /* RCTModuleIDs.m */, 83CBBA581A601E9000E9B192 /* RCTRedBox.h */, 83CBBA591A601E9000E9B192 /* RCTRedBox.m */, 83CBBA961A6020BB00E9B192 /* RCTTouchHandler.h */, @@ -439,7 +434,6 @@ 13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */, 137029531A69923600575408 /* RCTImageDownloader.m in Sources */, 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */, - 83CBBA8B1A60204600E9B192 /* RCTModuleIDs.m in Sources */, 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */, 13B080071A6947C200A75B9A /* RCTShadowRawText.m in Sources */, 13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */, @@ -555,6 +549,7 @@ "DEBUG=1", "$(inherited)", ); + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -564,6 +559,7 @@ 83CBBA411A601D0F00E9B192 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/ReactKit/Views/RCTNavItemManager.m b/ReactKit/Views/RCTNavItemManager.m index e509af80f..515b3437b 100644 --- a/ReactKit/Views/RCTNavItemManager.m +++ b/ReactKit/Views/RCTNavItemManager.m @@ -7,9 +7,9 @@ @implementation RCTNavItemManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { - return [[RCTNavItem alloc] initWithFrame:CGRectZero]; + return [[RCTNavItem alloc] init]; } RCT_EXPORT_VIEW_PROPERTY(title) diff --git a/ReactKit/Views/RCTNavigator.h b/ReactKit/Views/RCTNavigator.h index e88b45ce9..76c6deab9 100644 --- a/ReactKit/Views/RCTNavigator.h +++ b/ReactKit/Views/RCTNavigator.h @@ -9,8 +9,7 @@ @property (nonatomic, strong) UIView *reactNavSuperviewLink; @property (nonatomic, assign) NSInteger requestedTopOfStack; -- (instancetype)initWithFrame:(CGRect)frame - eventDispatcher:(RCTEventDispatcher *)eventDispatcher; +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; /** * Schedules a JavaScript navigation and prevents `UIKit` from navigating until diff --git a/ReactKit/Views/RCTNavigator.m b/ReactKit/Views/RCTNavigator.m index 97d545948..b5769540b 100644 --- a/ReactKit/Views/RCTNavigator.m +++ b/ReactKit/Views/RCTNavigator.m @@ -126,7 +126,17 @@ NSInteger kNeverProgressed = -10000; */ @implementation RCTNavigationController -- (instancetype)init +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + RCT_NOT_DESIGNATED_INITIALIZER(); +} + +- (instancetype)initWithNavigationBarClass:(Class)navigationBarClass toolbarClass:(Class)toolbarClass +{ + RCT_NOT_DESIGNATED_INITIALIZER(); +} + +- (instancetype)initWithRootViewController:(UIViewController *)rootViewController { RCT_NOT_DESIGNATED_INITIALIZER(); } @@ -137,7 +147,7 @@ NSInteger kNeverProgressed = -10000; */ - (instancetype)initWithScrollCallback:(dispatch_block_t)callback { - if ((self = [super init])) { + if ((self = [super initWithNibName:nil bundle:nil])) { _scrollCallback = callback; } return self; @@ -265,11 +275,14 @@ NSInteger kNeverProgressed = -10000; @implementation RCTNavigator -- (id)initWithFrame:(CGRect)frame - eventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { + RCT_NOT_DESIGNATED_INITIALIZER(); +} + +- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(reportNavigationProgress:)]; _mostRecentProgress = kNeverProgressed; _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; @@ -311,24 +324,15 @@ NSInteger kNeverProgressed = -10000; return; } _mostRecentProgress = nextProgress; - [_eventDispatcher sendRawEventWithType:@"topNavigationProgress" - body:@{@"fromIndex": @(_currentlyTransitioningFrom), - @"toIndex": @(_currentlyTransitioningTo), - @"progress": @(nextProgress), - @"target": self.reactTag}]; + [_eventDispatcher sendEventWithName:@"topNavigationProgress" body:@{ + @"fromIndex": @(_currentlyTransitioningFrom), + @"toIndex": @(_currentlyTransitioningTo), + @"progress": @(nextProgress), + @"target": self.reactTag + }]; } } -- (instancetype)initWithFrame:(CGRect)frame -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - -- (instancetype)init -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - - (void)dealloc { _navigationController.delegate = nil; @@ -438,9 +442,10 @@ NSInteger kNeverProgressed = -10000; - (void)handleTopOfStackChanged { - [_eventDispatcher sendRawEventWithType:@"topNavigateBack" - body:@{@"target":self.reactTag, - @"stackLength":@(_navigationController.viewControllers.count)}]; + [_eventDispatcher sendEventWithName:@"topNavigateBack" body:@{ + @"target":self.reactTag, + @"stackLength":@(_navigationController.viewControllers.count) + }]; } - (void)dispatchFakeScrollEvent diff --git a/ReactKit/Views/RCTNavigatorManager.m b/ReactKit/Views/RCTNavigatorManager.m index 317ff4865..973d4958d 100644 --- a/ReactKit/Views/RCTNavigatorManager.m +++ b/ReactKit/Views/RCTNavigatorManager.m @@ -8,9 +8,9 @@ @implementation RCTNavigatorManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { - return [[RCTNavigator alloc] initWithFrame:CGRectZero eventDispatcher:eventDispatcher]; + return [[RCTNavigator alloc] initWithEventDispatcher:self.eventDispatcher]; } RCT_EXPORT_VIEW_PROPERTY(requestedTopOfStack) diff --git a/ReactKit/Views/RCTNetworkImageView.m b/ReactKit/Views/RCTNetworkImageView.m index 3f6dc8ed4..2a739cea7 100644 --- a/ReactKit/Views/RCTNetworkImageView.m +++ b/ReactKit/Views/RCTNetworkImageView.m @@ -4,6 +4,7 @@ #import "RCTImageDownloader.h" #import "RCTUtils.h" +#import "RCTConvert.h" @implementation RCTNetworkImageView { @@ -52,13 +53,27 @@ self.layer.contentsScale = _defaultImage.scale; self.layer.contents = (__bridge id)_defaultImage.CGImage; } - _downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() block:^(UIImage *image, NSError *error) { - if (image) { - self.layer.contentsScale = image.scale; - self.layer.contents = (__bridge id)image.CGImage; - } - // TODO: handle errors - }]; + if ([imageURL.pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame) { + _downloadToken = [_imageDownloader downloadDataForURL:imageURL block:^(NSData *data, NSError *error) { + if (data) { + CAKeyframeAnimation *animation = [RCTConvert GIF:data]; + CGImageRef firstFrame = (__bridge CGImageRef)animation.values.firstObject; + self.layer.bounds = CGRectMake(0, 0, CGImageGetWidth(firstFrame), CGImageGetHeight(firstFrame)); + self.layer.contentsScale = 1.0; + self.layer.contentsGravity = kCAGravityResizeAspect; + [self.layer addAnimation:animation forKey:@"contents"]; + } + // TODO: handle errors + }]; + } else { + _downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() block:^(UIImage *image, NSError *error) { + if (image) { + self.layer.contentsScale = image.scale; + self.layer.contents = (__bridge id)image.CGImage; + } + // TODO: handle errors + }]; + } } } diff --git a/ReactKit/Views/RCTNetworkImageViewManager.m b/ReactKit/Views/RCTNetworkImageViewManager.m index b1235d82b..5f8ad5d35 100644 --- a/ReactKit/Views/RCTNetworkImageViewManager.m +++ b/ReactKit/Views/RCTNetworkImageViewManager.m @@ -11,7 +11,7 @@ @implementation RCTNetworkImageViewManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { RCTNetworkImageView *view = [[RCTNetworkImageView alloc] initWithFrame:CGRectZero imageDownloader:[RCTImageDownloader sharedInstance]]; view.contentMode = UIViewContentModeScaleAspectFill; diff --git a/ReactKit/Views/RCTRawTextManager.m b/ReactKit/Views/RCTRawTextManager.m index 1d007e88f..57ee75204 100644 --- a/ReactKit/Views/RCTRawTextManager.m +++ b/ReactKit/Views/RCTRawTextManager.m @@ -6,7 +6,7 @@ @implementation RCTRawTextManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { return [[UIView alloc] init]; } diff --git a/ReactKit/Views/RCTScrollView.h b/ReactKit/Views/RCTScrollView.h index 9de2892e6..82667b205 100644 --- a/ReactKit/Views/RCTScrollView.h +++ b/ReactKit/Views/RCTScrollView.h @@ -32,6 +32,6 @@ @property (nonatomic, assign) BOOL centerContent; @property (nonatomic, copy) NSArray *stickyHeaderIndices; -- (instancetype)initWithFrame:(CGRect)frame eventDispatcher:(RCTEventDispatcher *)eventDispatcher; +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; @end diff --git a/ReactKit/Views/RCTScrollView.m b/ReactKit/Views/RCTScrollView.m index 0af80319b..157cd406d 100644 --- a/ReactKit/Views/RCTScrollView.m +++ b/ReactKit/Views/RCTScrollView.m @@ -8,6 +8,7 @@ #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTUIManager.h" +#import "RCTUtils.h" #import "UIView+ReactKit.h" CGFloat const ZINDEX_DEFAULT = 0; @@ -122,6 +123,7 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; */ - (BOOL)touchesShouldCancelInContentView:(UIView *)view { + //TODO: shouldn't this call super if _shouldDisableScrollInteraction returns NO? return ![self _shouldDisableScrollInteraction]; } @@ -260,9 +262,14 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; @synthesize nativeMainScrollDelegate = _nativeMainScrollDelegate; -- (instancetype)initWithFrame:(CGRect)frame eventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithFrame:(CGRect)frame { - if ((self = [super initWithFrame:frame])) { + RCT_NOT_DESIGNATED_INITIALIZER(); +} + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = eventDispatcher; _scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero]; diff --git a/ReactKit/Views/RCTScrollViewManager.m b/ReactKit/Views/RCTScrollViewManager.m index 1db32aae6..5100d1186 100644 --- a/ReactKit/Views/RCTScrollViewManager.m +++ b/ReactKit/Views/RCTScrollViewManager.m @@ -7,9 +7,9 @@ @implementation RCTScrollViewManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { - return [[RCTScrollView alloc] initWithFrame:CGRectZero eventDispatcher:eventDispatcher]; + return [[RCTScrollView alloc] initWithEventDispatcher:self.eventDispatcher]; } RCT_EXPORT_VIEW_PROPERTY(alwaysBounceHorizontal) @@ -36,7 +36,7 @@ RCT_EXPORT_VIEW_PROPERTY(contentInset); RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets); RCT_EXPORT_VIEW_PROPERTY(contentOffset); -- (NSDictionary *)constantsToExport ++ (NSDictionary *)constantsToExport { return @{ diff --git a/ReactKit/Views/RCTStaticImageManager.m b/ReactKit/Views/RCTStaticImageManager.m index 86eac9a12..f4f86abb5 100644 --- a/ReactKit/Views/RCTStaticImageManager.m +++ b/ReactKit/Views/RCTStaticImageManager.m @@ -9,14 +9,26 @@ @implementation RCTStaticImageManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { return [[RCTStaticImage alloc] init]; } -RCT_REMAP_VIEW_PROPERTY(src, image) RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode) +- (void)set_src:(id)json forView:(RCTStaticImage *)view withDefaultView:(RCTStaticImage *)defaultView +{ + if (json) { + if ([json isKindOfClass:[NSString class]] && [[json pathExtension] caseInsensitiveCompare:@"gif"] == NSOrderedSame) { + [view.layer addAnimation:[RCTConvert GIF:json] forKey:@"contents"]; + } else { + view.image = [RCTConvert UIImage:json]; + } + } else { + view.image = defaultView.image; + } +} + - (void)set_capInsets:(id)json forView:(RCTStaticImage *)view withDefaultView:(RCTStaticImage *)defaultView { view.capInsets = json ? [RCTConvert UIEdgeInsets:json] : defaultView.capInsets; diff --git a/ReactKit/Views/RCTTextField.h b/ReactKit/Views/RCTTextField.h index 45bacd8ac..2a0225f27 100644 --- a/ReactKit/Views/RCTTextField.h +++ b/ReactKit/Views/RCTTextField.h @@ -10,6 +10,6 @@ @property (nonatomic, assign) BOOL autoCorrect; @property (nonatomic, assign) UIEdgeInsets paddingEdgeInsets; // TODO: contentInset -- (instancetype)initWithFrame:(CGRect)frame eventDispatcher:(RCTEventDispatcher *)eventDispatcher; +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; @end diff --git a/ReactKit/Views/RCTTextField.m b/ReactKit/Views/RCTTextField.m index 50f8170e7..4a70e0460 100644 --- a/ReactKit/Views/RCTTextField.m +++ b/ReactKit/Views/RCTTextField.m @@ -14,14 +14,14 @@ BOOL _jsRequestingFirstResponder; } -- (instancetype)init +- (instancetype)initWithFrame:(CGRect)frame { RCT_NOT_DESIGNATED_INITIALIZER(); } -- (instancetype)initWithFrame:(CGRect)frame eventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { - if ((self = [super initWithFrame:frame])) { + if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = eventDispatcher; [self addTarget:self action:@selector(_textFieldDidChange) forControlEvents:UIControlEventEditingChanged]; diff --git a/ReactKit/Views/RCTTextFieldManager.m b/ReactKit/Views/RCTTextFieldManager.m index cb037ec2e..339a7803e 100644 --- a/ReactKit/Views/RCTTextFieldManager.m +++ b/ReactKit/Views/RCTTextFieldManager.m @@ -8,9 +8,9 @@ @implementation RCTTextFieldManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { - return [[RCTTextField alloc] initWithFrame:CGRectZero eventDispatcher:eventDispatcher]; + return [[RCTTextField alloc] initWithEventDispatcher:self.eventDispatcher]; } RCT_EXPORT_VIEW_PROPERTY(caretHidden) diff --git a/ReactKit/Views/RCTTextManager.m b/ReactKit/Views/RCTTextManager.m index 8a2f5bba6..f7f096dba 100644 --- a/ReactKit/Views/RCTTextManager.m +++ b/ReactKit/Views/RCTTextManager.m @@ -13,7 +13,7 @@ @implementation RCTTextManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { UILabel *label = [[UILabel alloc] init]; label.numberOfLines = 0; diff --git a/ReactKit/Views/RCTUIActivityIndicatorViewManager.m b/ReactKit/Views/RCTUIActivityIndicatorViewManager.m index 7cc352e35..b7e0971e7 100644 --- a/ReactKit/Views/RCTUIActivityIndicatorViewManager.m +++ b/ReactKit/Views/RCTUIActivityIndicatorViewManager.m @@ -6,9 +6,9 @@ @implementation RCTUIActivityIndicatorViewManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { - return [[UIActivityIndicatorView alloc] initWithFrame:CGRectZero]; + return [[UIActivityIndicatorView alloc] init]; } RCT_EXPORT_VIEW_PROPERTY(activityIndicatorViewStyle) @@ -28,7 +28,7 @@ RCT_EXPORT_VIEW_PROPERTY(color) } } -- (NSDictionary *)constantsToExport ++ (NSDictionary *)constantsToExport { return @{ diff --git a/ReactKit/Views/RCTUIViewManager.h b/ReactKit/Views/RCTUIViewManager.h index 92d7e1910..acec4c78a 100644 --- a/ReactKit/Views/RCTUIViewManager.h +++ b/ReactKit/Views/RCTUIViewManager.h @@ -4,6 +4,12 @@ #import "RCTExport.h" +@class RCTEventDispatcher; + @interface RCTUIViewManager : NSObject +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, readonly) RCTEventDispatcher *eventDispatcher; + @end diff --git a/ReactKit/Views/RCTUIViewManager.m b/ReactKit/Views/RCTUIViewManager.m index dbdcb5473..b2dc053b3 100644 --- a/ReactKit/Views/RCTUIViewManager.m +++ b/ReactKit/Views/RCTUIViewManager.m @@ -3,13 +3,38 @@ #import "RCTUIViewManager.h" #import "RCTConvert.h" +#import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTShadowView.h" #import "RCTView.h" @implementation RCTUIViewManager +{ + __weak RCTEventDispatcher *_eventDispatcher; +} -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super init])) { + _eventDispatcher = eventDispatcher; + } + return self; +} + ++ (NSString *)moduleName +{ + // Default implementation, works in most cases + NSString *name = NSStringFromClass(self); + if ([name hasPrefix:@"RCTUI"]) { + name = [name substringFromIndex:@"RCT".length]; + } + if ([name hasSuffix:@"Manager"]) { + name = [name substringToIndex:name.length - @"Manager".length]; + } + return name; +} + +- (UIView *)view { return [[UIView alloc] init]; } @@ -29,7 +54,7 @@ RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius) RCT_REMAP_VIEW_PROPERTY(borderColor, layer.borderColor); RCT_REMAP_VIEW_PROPERTY(borderRadius, layer.cornerRadius) RCT_REMAP_VIEW_PROPERTY(borderWidth, layer.borderWidth) -RCT_REMAP_VIEW_PROPERTY(transformMatrix, view.layer.transform) +RCT_REMAP_VIEW_PROPERTY(transformMatrix, layer.transform) - (void)set_overflow:(id)json forView:(UIView *)view diff --git a/ReactKit/Views/RCTViewManager.m b/ReactKit/Views/RCTViewManager.m index 5acfa7332..7add6d617 100644 --- a/ReactKit/Views/RCTViewManager.m +++ b/ReactKit/Views/RCTViewManager.m @@ -6,12 +6,11 @@ @implementation RCTViewManager -- (UIView *)viewWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (UIView *)view { return [[RCTView alloc] init]; } -RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel) RCT_EXPORT_VIEW_PROPERTY(pointerEvents) @end diff --git a/ReactKit/Views/RCTWrapperViewController.m b/ReactKit/Views/RCTWrapperViewController.m index b638d2b50..b5425fbcd 100644 --- a/ReactKit/Views/RCTWrapperViewController.m +++ b/ReactKit/Views/RCTWrapperViewController.m @@ -96,7 +96,7 @@ - (void)rightButtonTapped { - [_eventDispatcher sendRawEventWithType:@"topNavRightButtonTap" body:@{@"target":_navItem.reactTag}]; + [_eventDispatcher sendEventWithName:@"topNavRightButtonTap" body:@{@"target":_navItem.reactTag}]; } - (void)didMoveToParentViewController:(UIViewController *)parent