diff --git a/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js b/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js index 2517f46d8e..141b0a9700 100644 --- a/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js +++ b/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js @@ -16,7 +16,7 @@ * @providesModule ResponderEventPlugin */ -"use strict"; +'use strict'; var EventConstants = require('EventConstants'); var EventPluginUtils = require('EventPluginUtils'); @@ -27,6 +27,7 @@ var ResponderSyntheticEvent = require('ResponderSyntheticEvent'); var ResponderTouchHistoryStore = require('ResponderTouchHistoryStore'); var accumulate = require('accumulate'); +var invariant = require('invariant'); var keyOf = require('keyOf'); var isStartish = EventPluginUtils.isStartish; @@ -43,6 +44,12 @@ var executeDispatchesInOrderStopAtTrue = */ var responderID = null; +/** + * Count of current touches. A textInput should become responder iff the + * the selection changes while there is a touch on the screen. + */ +var trackedTouchCount = 0; + /** * Last reported number of active touches. */ @@ -426,7 +433,8 @@ function setResponderAndExtractTransfer( function canTriggerTransfer(topLevelType, topLevelTargetID) { return topLevelTargetID && ( topLevelType === EventConstants.topLevelTypes.topScroll || - topLevelType === EventConstants.topLevelTypes.topSelectionChange || + (trackedTouchCount > 0 && + topLevelType === EventConstants.topLevelTypes.topSelectionChange) || isStartish(topLevelType) || isMoveish(topLevelType) ); @@ -489,6 +497,15 @@ var ResponderEventPlugin = { topLevelTargetID, nativeEvent) { + if (isStartish(topLevelType)) { + trackedTouchCount += 1; + } else if (isEndish(topLevelType)) { + trackedTouchCount -= 1; + invariant( + trackedTouchCount >= 0, + 'Ended a touch event which was not counted in trackedTouchCount.' + ); + } ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent); @@ -507,12 +524,10 @@ var ResponderEventPlugin = { // (`onResponderRelease/onResponderTerminate`). var isResponderTouchStart = responderID && isStartish(topLevelType); var isResponderTouchMove = responderID && isMoveish(topLevelType); - var isResponderTouchTerminate = responderID && topLevelType === EventConstants.topLevelTypes.topTouchCancel; var isResponderTouchEnd = responderID && isEndish(topLevelType); var incrementalTouch = isResponderTouchStart ? eventTypes.responderStart : isResponderTouchMove ? eventTypes.responderMove : - isResponderTouchTerminate ? eventTypes.responderTerminate : isResponderTouchEnd ? eventTypes.responderEnd : null; @@ -524,17 +539,24 @@ var ResponderEventPlugin = { extracted = accumulate(extracted, gesture); } + var isResponderTerminate = + responderID && + topLevelType === EventConstants.topLevelTypes.topTouchCancel; var isResponderRelease = - responderID && isEndish(topLevelType) && noResponderTouches(nativeEvent); - if (isResponderRelease) { - var release = ResponderSyntheticEvent.getPooled( - eventTypes.responderRelease, - responderID, - nativeEvent - ); - release.touchHistory = ResponderTouchHistoryStore.touchHistory; - EventPropagators.accumulateDirectDispatches(release); - extracted = accumulate(extracted, release); + responderID && + !isResponderTerminate && + isEndish(topLevelType) && + noResponderTouches(nativeEvent); + var finalTouch = + isResponderTerminate ? eventTypes.responderTerminate : + isResponderRelease ? eventTypes.responderRelease : + null; + if (finalTouch) { + var finalEvent = + ResponderSyntheticEvent.getPooled(finalTouch, responderID, nativeEvent); + finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; + EventPropagators.accumulateDirectDispatches(finalEvent); + extracted = accumulate(extracted, finalEvent); changeResponder(null); } diff --git a/Libraries/vendor/react/vendor/core/ExecutionEnvironment.ios.js b/Libraries/vendor/react/vendor/core/ExecutionEnvironment.ios.js new file mode 100644 index 0000000000..3a59accdcd --- /dev/null +++ b/Libraries/vendor/react/vendor/core/ExecutionEnvironment.ios.js @@ -0,0 +1,48 @@ +/** + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ExecutionEnvironment + * + * Stubs SignedSource<<7afee88a05412d0c4eef54817419648e>> + */ + +/*jslint evil: true */ + +"use strict"; + +var canUseDOM = false; + +/** + * Simple, lightweight module assisting with the detection and context of + * Worker. Helps avoid circular dependencies and allows code to reason about + * whether or not they are in a Worker, even if they never include the main + * `ReactWorker` dependency. + */ +var ExecutionEnvironment = { + + canUseDOM: canUseDOM, + + canUseWorkers: typeof Worker !== 'undefined', + + canUseEventListeners: + canUseDOM && !!(window.addEventListener || window.attachEvent), + + canUseViewport: canUseDOM && !!window.screen, + + isInWorker: !canUseDOM // For now, this is true - might change in the future. + +}; + +module.exports = ExecutionEnvironment; diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js index d527e3c92c..5df94697ed 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -2,13 +2,12 @@ * @providesModule Touchable */ -"use strict"; +'use strict'; var BoundingDimensions = require('BoundingDimensions'); var Position = require('Position'); var TouchEventUtils = require('TouchEventUtils'); -var invariant = require('invariant'); var keyMirror = require('keyMirror'); var queryLayoutByID = require('queryLayoutByID'); @@ -277,20 +276,20 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10; * + * | RESPONDER_GRANT (HitRect) * v - * +---------------------------+ DELAY +-------------------------+ T - DELAY +------------------------------+ - * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN| + * +---------------------------+ DELAY +-------------------------+ T - DELAY +------------------------------+ + * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN| * +---------------------------+ +-------------------------+ +------------------------------+ * + ^ + ^ + ^ * |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ * |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT * | | | | | | * v + v + v + - * +----------------------------+ DELAY +--------------------------+ +-------------------------------+ - * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT| - * +----------------------------+ +--------------------------+ +-------------------------------+ + * +----------------------------+ DELAY +--------------------------+ +-------------------------------+ + * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT| + * +----------------------------+ +--------------------------+ +-------------------------------+ + * + * T - DELAY => LONG_PRESS_THRESHOLD - DELAY * - * T - DELAY => LONG_PRESS_THRESHOLD - DELAY - * * Not drawn are the side effects of each transition. The most important side * effect is the `touchableHandlePress` abstract method invocation that occurs * when a responder is released while in either of the "Press" states. @@ -344,7 +343,7 @@ var TouchableMixin = { * */ touchableHandleResponderGrant: function(e, dispatchID) { - // Since e is used in a callback invoked on another event loop + // Since e is used in a callback invoked on another event loop // (as in setTimeout etc), we need to call e.persist() on the // event to make sure it doesn't get reused in the event object pool. e.persist(); @@ -420,9 +419,9 @@ var TouchableMixin = { var movedDistance = this._getDistanceBetweenPoints(pageX, pageY, this.pressInLocation.pageX, this.pressInLocation.pageY); if (movedDistance > LONG_PRESS_ALLOWED_MOVEMENT) { this._cancelLongPressDelayTimeout(); - } + } } - + var isTouchWithinActive = pageX > positionOnActivate.left - pressExpandLeft && pageY > positionOnActivate.top - pressExpandTop && @@ -556,18 +555,19 @@ var TouchableMixin = { */ _receiveSignal: function(signal, e) { var curState = this.state.touchable.touchState; - invariant( - Transitions[curState] && Transitions[curState][signal], - 'You have supplied either an unrecognized signal or current state %s', - curState - ); + if (!(Transitions[curState] && Transitions[curState][signal])) { + throw new Error( + 'Unrecognized signal `' + signal + '` or state `' + curState + + '` for Touchable responder `' + this.state.touchable.responderID + '`' + ); + } var nextState = Transitions[curState][signal]; - invariant( - nextState !== States.ERROR, - 'Some assumptions about the state machine were violated. This is the ' + - 'fault of Touchable.js. This case has been modeled and caught as an ' + - 'error transition.' - ); + if (nextState === States.ERROR) { + throw new Error( + 'Touchable cannot transition from `' + curState + '` to `' + signal + + '` for responder `' + this.state.touchable.responderID + '`' + ); + } if (curState !== nextState) { this._performSideEffectsForTransition(curState, nextState, signal, e); this.state.touchable.touchState = nextState; @@ -580,7 +580,7 @@ var TouchableMixin = { }, _isHighlight: function (state) { - return state === States.RESPONDER_ACTIVE_PRESS_IN || + return state === States.RESPONDER_ACTIVE_PRESS_IN || state === States.RESPONDER_ACTIVE_LONG_PRESS_IN; }, @@ -626,15 +626,15 @@ var TouchableMixin = { if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) { this.touchableHandleLongPress && this.touchableHandleLongPress(); - } - + } + if (newIsHighlight && !curIsHighlight) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(); } else if (!newIsHighlight && curIsHighlight) { this.touchableHandleActivePressOut && this.touchableHandleActivePressOut(); } - + if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) { var hasLongPressHandler = !!this.touchableHandleLongPress; var pressIsLongButStillCallOnPress = @@ -660,4 +660,3 @@ var Touchable = { }; module.exports = Touchable; - diff --git a/README.md b/README.md index 58bd93fe31..2ef3094f71 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -**Warning: This is a one-time code drop to accompany the ReactJS conference +**Warning: This is currently a private repo to accompany the ReactJS conference talk, and is not accessible outside the official conference attendees - please -don't share this code.** +do not share this code.** This is also a very early alpha release. There are certainly bugs and missing features. Some things may even be well-documented in JS, but missing from the @@ -67,11 +67,14 @@ the same library.** ## Troubleshooting -Xcode will break if you have two examples open at the same time. - -Jest testing does not yet work on node versions after 0.10.x. - -You can verify the packager is working by loading the [bundle](http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle) in your browser and ++ Xcode will break if you have two examples open at the same time. ++ If `npm start` fails with log spew like: + ``` + 2015-02-02 10:56 node[24294] (FSEvents.framework) FSEventStreamStart: register_with_server: ERROR: f2d_register_rpc() => (null) (-21) + ``` +then you've hit the node file watching bug - `brew install watchman` should fix the issue. ++ Jest testing does not yet work on node versions after 0.10.x. ++ You can verify the packager is working by loading the [bundle](http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle) in your browser and inspecting the contents. Please report any other issues you encounter so we can fix them ASAP. @@ -92,27 +95,27 @@ the responder system. # FAQ -Q. How does debugging work? Can I set breakpoints in my JS? +Q. How does debugging work? Can I set breakpoints in my JS? A. We are going to add the ability to use the Chrome developer tools soon. We are very passionate about building the best possible developer experience. -Q. When is this coming to Android/Windows/OS X/etc? +Q. When is this coming to Android/Windows/OS X/etc? A. We're working on Android, and we are excited to release it as soon as we can. We are looking forward to the community helping us target other platforms as well :) -Q. How do I create my own app? +Q. How do I create my own app? A. Copy the entire `Examples/TicTacToe` folder, rename stuff in Xcode, and replace the `TicTacToeApp.js` with your own. Then, in `AppDelegate.m`, update `moduleName` to match your call to `Bundler.registerComponent(, )` at the bottom of your JS file, and update `jsCodeLocation` to match your JS file name and location. -Q. Can I submit my own React Native app to the App Store? +Q. Can I submit my own React Native app to the App Store? A. Not yet, but you will be able to soon. If you build something you want to submit to the App Store, come talk to us ASAP. -Q. How do I deploy to my device? +Q. How do I deploy to my device? A. You can change `localhost` in `AppDelegate.m` to your laptop's IP address and grab the bundle over the same Wi-Fi network. You can also download the bundle that the React packager generates, save it to the file `main.jsbundle`, and add it @@ -120,20 +123,20 @@ as a static resource in your Xcode project. Then set the `jsCodeLocation` in `AppDelegate.m` to point to that file and deploy to your device like you would any other app. -Q. What's up with this private repo? Why aren't you just open sourcing it now? +Q. What's up with this private repo? Why aren't you just open sourcing it now? A. We want input from the React community before we open the floodgates so we can incorporate your feedback, and we also have a bunch more features we want to add to make a more complete offering before we open source. -Q. Do you have to ship a JS runtime with your apps? +Q. Do you have to ship a JS runtime with your apps? A. No, we just use the JavaScriptCore public API that is part of iOS 7 and later. -Q. How do I add more native capabilities? +Q. How do I add more native capabilities? A. React Native is designed to be extensible - come talk to us, we would love to work with you. -Q. Can I reuse existing iOS code? +Q. Can I reuse existing iOS code? A. Yes, React Native is designed to be extensible and allow integration of all sorts of native components, such as `UINavigationController` (available as ``), `MKMapView` (not available yet), or your own custom diff --git a/ReactKit/Base/RCTBridge.h b/ReactKit/Base/RCTBridge.h index 1cb53fc0a0..48c3f77f27 100644 --- a/ReactKit/Base/RCTBridge.h +++ b/ReactKit/Base/RCTBridge.h @@ -1,11 +1,9 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTExport.h" +#import "RCTBridgeModule.h" #import "RCTInvalidating.h" #import "RCTJavaScriptExecutor.h" -@protocol RCTNativeModule; - @class RCTEventDispatcher; @class RCTRootView; @@ -29,31 +27,65 @@ static inline NSDictionary *RCTAPIErrorObject(NSString *msg) } /** - * Async batched bridge used to communicate with `RCTJavaScriptAppEngine`. + * Async batched bridge used to communicate with the JavaScript application. */ @interface RCTBridge : NSObject -- (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor; +/** + * The designated initializer. This creates a new bridge on top of the specified + * executor. The bridge should then be used for all subsequent communication + * with the JavaScript code running in the executor. + */ +- (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor NS_DESIGNATED_INITIALIZER; +/** + * This method is used to call functions in the JavaScript application context. + * It is primarily intended for use by modules that require two-way communication + * with the JavaScript code. + */ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args; + +/** + * This method is used to execute a new application script. It is called + * internally whenever a JS application bundle is loaded/reloaded, but should + * probably not be used at any other time. + */ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete; +/** + * The event dispatcher is a wrapper around -enqueueJSCall:args: that provides a + * higher-level interface for sending UI events such as touches and text input. + */ @property (nonatomic, readonly) RCTEventDispatcher *eventDispatcher; + +/** + * The shadow queue is used to execute callbacks from the JavaScript code. All + * native hooks (e.g. exported module methods) will be executed on the shadow + * queue. + */ @property (nonatomic, readonly) dispatch_queue_t shadowQueue; // For use in implementing delegates, which may need to queue responses. - (RCTResponseSenderBlock)createResponseSenderBlock:(NSInteger)callbackID; +/** + * Register a root view with the bridge. Theorectically, a single bridge can + * support multiple root views, however this feature is not currently exposed + * and may eventually be removed. + */ - (void)registerRootView:(RCTRootView *)rootView; /** - * Global logging function will print to both xcode and js debugger consoles. + * Global logging function that 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; +/** + * Method to check that a valid executor exists with which to log + */ + (BOOL)hasValidJSExecutor; @end diff --git a/ReactKit/Base/RCTBridge.m b/ReactKit/Base/RCTBridge.m index 3604b9dff7..20acd088a2 100644 --- a/ReactKit/Base/RCTBridge.m +++ b/ReactKit/Base/RCTBridge.m @@ -2,27 +2,18 @@ #import "RCTBridge.h" +#import +#import +#import #import +#import -#import "RCTModuleMethod.h" +#import "RCTConvert.h" #import "RCTInvalidating.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTUtils.h" -/** - * 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`. */ @@ -35,13 +26,148 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; -static NSDictionary *RCTNativeModuleClasses(void) +/** + * This private class is used as a container for exported method info + */ +@interface RCTModuleMethod : NSObject + +@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 + +@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 + +#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 + +/** + * This function parses the exported methods inside RCTBridgeModules and + * generates a dictionary of arrays of RCTModuleMethod objects, keyed + * by module name. + */ +static NSDictionary *RCTExportedMethodsByModule(void) +{ + static NSMutableDictionary *methodsByModule; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + Dl_info info; + dladdr(&RCTExportedMethodsByModule, &info); + + const RCTExportValue mach_header = (RCTExportValue)info.dli_fbase; + const RCTExportSection *section = RCTGetSectByNameFromHeader((void *)mach_header, "__DATA", "RCTExport"); + + if (section == NULL) { + return; + } + + methodsByModule = [NSMutableDictionary dictionary]; + NSCharacterSet *plusMinusCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"+-"]; + + for (RCTExportValue addr = section->offset; + addr < section->offset + section->size; + addr += sizeof(id) * 2) { + + const char **entry = (const char **)(mach_header + addr); + NSScanner *scanner = [NSScanner scannerWithString:@(entry[0])]; + + 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; + + 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); + } + + NSString *JSMethodName = strlen(entry[1]) ? @(entry[1]) : [NSStringFromSelector(selector) componentsSeparatedByString:@":"][0]; + RCTModuleMethod *moduleMethod = + [[RCTModuleMethod alloc] initWithSelector:selector + JSMethodName:JSMethodName + arity:method_getNumberOfArguments(method) - 2 + blockArgumentIndexes:blockArgumentIndexes]; + + NSString *moduleName = [class respondsToSelector:@selector(moduleName)] ? [class moduleName] : className; + NSArray *moduleMap = methodsByModule[moduleName]; + methodsByModule[moduleName] = (moduleMap != nil) ? [moduleMap arrayByAddingObject:moduleMethod] : @[moduleMethod]; + } + + }); + + return methodsByModule; +} + +/** + * This function scans all classes available at runtime and returns a dictionary + * of classes that implement the RTCBridgeModule protocol, keyed by module name. + */ +static NSDictionary *RCTBridgeModuleClasses(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++) { @@ -53,17 +179,17 @@ static NSDictionary *RCTNativeModuleClasses(void) continue; } - if (![cls conformsToProtocol:@protocol(RCTNativeModule)]) { - // Not an RCTNativeModule + if (![cls conformsToProtocol:@protocol(RCTBridgeModule)]) { + // Not an RCTBridgeModule continue; } // Get module name NSString *moduleName = [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(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); + RCTCAssert(existingClass == Nil, @"Attempted to register RCTBridgeModule class %@ for the name '%@', but name was already registered by class %@", cls, moduleName, existingClass); modules[moduleName] = cls; } @@ -73,11 +199,166 @@ static NSDictionary *RCTNativeModuleClasses(void) return modules; } +/** + * 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... + */ +static NSMutableDictionary *RCTRemoteModulesByID; +static NSDictionary *RCTRemoteModulesConfig() +{ + static NSMutableDictionary *remoteModules; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + RCTRemoteModulesByID = [[NSMutableDictionary alloc] init]; + + remoteModules = [[NSMutableDictionary alloc] init]; + [RCTExportedMethodsByModule() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, NSArray *rawMethods, BOOL *stop) { + + NSMutableDictionary *methods = [NSMutableDictionary dictionaryWithCapacity:rawMethods.count]; + [rawMethods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *stop) { + methods[method.JSMethodName] = @{ + @"methodID": @(methodID), + @"type": @"remote", + }; + }]; + + NSDictionary *module = @{ + @"moduleID": @(remoteModules.count), + @"methods": methods + }; + + Class cls = RCTBridgeModuleClasses()[moduleName]; + if (RCTClassOverridesClassMethod(cls, @selector(constantsToExport))) { + module = [module mutableCopy]; + ((NSMutableDictionary *)module)[@"constants"] = [cls constantsToExport]; + } + remoteModules[moduleName] = module; + + // Add module lookup + RCTRemoteModulesByID[module[@"moduleID"]] = moduleName; + + }]; + }); + + return remoteModules; +} + +/** + * 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... + */ +static NSMutableDictionary *RCTLocalModuleIDs; +static NSMutableDictionary *RCTLocalMethodIDs; +static NSDictionary *RCTLocalModulesConfig() +{ + static NSMutableDictionary *localModules; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + RCTLocalModuleIDs = [[NSMutableDictionary alloc] init]; + RCTLocalMethodIDs = [[NSMutableDictionary alloc] init]; + + NSMutableArray *JSMethods = [[NSMutableArray alloc] init]; + + // Add globally used methods + [JSMethods 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 RCTBridgeModuleClasses().allValues) { + if (RCTClassOverridesClassMethod(cls, @selector(JSMethods))) { + [JSMethods addObjectsFromArray:[cls JSMethods]]; + } + } + + localModules = [[NSMutableDictionary alloc] init]; + for (NSString *moduleDotMethod in JSMethods) { + + NSArray *parts = [moduleDotMethod componentsSeparatedByString:@"."]; + RCTCAssert(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 + RCTLocalModuleIDs[moduleDotMethod] = module[@"moduleID"]; + RCTLocalMethodIDs[moduleDotMethod] = methods[methodName][@"methodID"]; + } + }); + + return localModules; +} + @implementation RCTBridge { NSMutableDictionary *_moduleInstances; - NSMutableDictionary *_moduleIDLookup; - NSMutableDictionary *_methodIDLookup; id _javaScriptExecutor; } @@ -90,10 +371,10 @@ static id _latestJSExecutor; _latestJSExecutor = _javaScriptExecutor; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; _shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL); - + // Instantiate modules _moduleInstances = [[NSMutableDictionary alloc] init]; - [RCTNativeModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) { + [RCTBridgeModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) { if (_moduleInstances[moduleName] == nil) { if ([moduleClass instancesRespondToSelector:@selector(initWithBridge:)]) { _moduleInstances[moduleName] = [[moduleClass alloc] initWithBridge:self]; @@ -103,17 +384,27 @@ static id _latestJSExecutor; } }]; - _moduleIDLookup = [[NSMutableDictionary alloc] init]; - _methodIDLookup = [[NSMutableDictionary alloc] init]; - [self doneRegisteringModules]; + // Inject module data into JS context + NSString *configJSON = RCTJSONStringify(@{ + @"remoteModuleConfig": RCTRemoteModulesConfig(), + @"localModulesConfig": RCTLocalModulesConfig() + }, NULL); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:^(id err) { + dispatch_semaphore_signal(semaphore); + }]; + + if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) { + RCTLogMustFix(@"JavaScriptExecutor took too long to inject JSON object"); + } } - + return self; } - (void)dealloc { - RCTAssert(!self.valid, @"must call -invalidate before -dealloc; TODO: why not call it here then?"); + RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); } #pragma mark - RCTInvalidating @@ -129,7 +420,7 @@ static id _latestJSExecutor; _latestJSExecutor = nil; } _javaScriptExecutor = nil; - + dispatch_sync(_shadowQueue, ^{ // Make sure all dispatchers have been executed before continuing // TODO: is this still needed? @@ -157,11 +448,11 @@ static id _latestJSExecutor; */ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args { - NSNumber *moduleID = _moduleIDLookup[moduleDotMethod]; + NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod]; RCTAssert(moduleID, @"Module '%@' not registered.", [[moduleDotMethod componentsSeparatedByString:@"."] firstObject]); - NSNumber *methodID = _methodIDLookup[moduleDotMethod]; + NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID, @"Method '%@' not registered.", moduleDotMethod); [self _invokeAndProcessModule:@"BatchedBridge" @@ -177,7 +468,7 @@ static id _latestJSExecutor; onComplete(scriptLoadError); return; } - + [_javaScriptExecutor executeJSCall:@"BatchedBridge" method:@"flushedQueue" arguments:@[] @@ -193,19 +484,19 @@ static id _latestJSExecutor; - (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 @@ -229,12 +520,12 @@ static id _latestJSExecutor; 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; @@ -242,7 +533,7 @@ static id _latestJSExecutor; 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]]) { @@ -250,27 +541,27 @@ static id _latestJSExecutor; return; } } - + NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs]; NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; - + NSUInteger numRequests = [moduleIDs count]; BOOL allSame = numRequests == [methodIDs count] && numRequests == [paramsArrays 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[i] integerValue] + moduleID:moduleIDs[i] methodID:[methodIDs[i] integerValue] params:paramsArrays[i]]; } } - + // TODO: only used by RCTUIManager - can we eliminate this special case? dispatch_async(_shadowQueue, ^{ for (id target in _moduleInstances.objectEnumerator) { @@ -282,7 +573,7 @@ static id _latestJSExecutor; } - (BOOL)_handleRequestNumber:(NSUInteger)i - moduleID:(NSInteger)moduleID + moduleID:(NSNumber *)moduleID methodID:(NSInteger)methodID params:(NSArray *)params { @@ -291,13 +582,15 @@ static id _latestJSExecutor; return NO; } - if (moduleID < 0 || moduleID >= RCTExportedMethodsByModule().count) { + NSString *moduleName = RCTRemoteModulesByID[moduleID]; + if (!moduleName) { + RCTLogMustFix(@"Unknown moduleID: %@", moduleID); return NO; } - NSString *moduleName = RCTExportedModuleNameAtSortedIndex(moduleID); NSArray *methods = RCTExportedMethodsByModule()[moduleName]; - if (methodID < 0 || methodID >= methods.count) { + if (methodID >= methods.count) { + RCTLogMustFix(@"Unknown methodID: %zd for module: %@", methodID, moduleName); return NO; } @@ -321,13 +614,12 @@ static id _latestJSExecutor; // invocation should not continue. return; } - + // 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); SEL selector = method.selector; - NSMethodSignature *methodSignature = [target methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [invocation setArgument:&target atIndex:0]; @@ -335,7 +627,7 @@ static id _latestJSExecutor; // Retain used blocks until after invocation completes. 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; @@ -344,9 +636,11 @@ static id _latestJSExecutor; [blocks addObject:block]; param = block; } - + NSUInteger argIdx = idx + 2; - + + // TODO: can we do this lookup in advance and cache the logic instead of + // recalculating it every time for every parameter? BOOL shouldSet = YES; const char *argumentType = [methodSignature getArgumentTypeAtIndex:argIdx]; switch (argumentType[0]) { @@ -357,7 +651,7 @@ static id _latestJSExecutor; shouldSet = NO; } break; - + case '*': if ([param isKindOfClass:[NSString class]]) { const char *string = [param UTF8String]; @@ -365,36 +659,38 @@ static id _latestJSExecutor; shouldSet = NO; } break; - + + // TODO: it seems like an error if the param doesn't respond + // so we should probably surface that error rather than failing silently #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) - +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]; + [invocation setArgument:¶m atIndex:argIdx]; } }]; @@ -420,7 +716,7 @@ static id _latestJSExecutor; if (!cbID) { return nil; } - + return ^(NSArray *args) { [self _sendResponseToJavaScriptCallbackID:cbID args:args]; }; @@ -433,7 +729,7 @@ static id _latestJSExecutor; dispatch_once(&onceToken, ^{ invocations = [NSMutableDictionary dictionary]; }); - + id key = @(argCount); NSInvocation *invocation = invocations[key]; if (invocation == nil) { @@ -442,170 +738,10 @@ static id _latestJSExecutor; invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; invocations[key] = invocation; } - + 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(); - - /** - * 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 *remoteModules = [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", - }; - }]; - - NSDictionary *module = @{ - @"moduleID": @(i), - @"methods": methods - }; - - id target = _moduleInstances[moduleName]; - if (RCTClassOverridesClassMethod([target class], @selector(constantsToExport))) { - module = [module mutableCopy]; - ((NSMutableDictionary *)module)[@"constants"] = [[target class] constantsToExport]; - } - remoteModules[moduleName] = module; - } - - /** - * 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); - [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:^(id err) { - dispatch_semaphore_signal(semaphore); - }]; - - if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) { - RCTLogMustFix(@"JavaScriptExecutor take too long to inject JSON object"); - } -} - - (void)registerRootView:(RCTRootView *)rootView { // TODO: only used by RCTUIManager - can we eliminate this special case? @@ -628,7 +764,7 @@ static id _latestJSExecutor; 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]]) { diff --git a/ReactKit/Base/RCTBridgeModule.h b/ReactKit/Base/RCTBridgeModule.h new file mode 100644 index 0000000000..1f95960b1a --- /dev/null +++ b/ReactKit/Base/RCTBridgeModule.h @@ -0,0 +1,65 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@class RCTBridge; + +/** + * 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 RCTBridgeModule +@optional + +/** + * Optional initializer for modules that require access + * to bridge features, such as sending events or making JS calls + */ +- (instancetype)initWithBridge:(RCTBridge *)bridge; + +/** + * The module name exposed to JS. If omitted, this will be inferred + * automatically by using the native module's class name. + */ ++ (NSString *)moduleName; + +/** + * 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 method name + * (the method will be namespaced to the module name, as specified above). + * If omitted, the JS method name will match the first part of the Objective-C + * method selector name (up to the first colon). + */ +#define RCT_EXPORT(js_name) __attribute__((used, section("__DATA,RCTExport" \ +))) static const char *__rct_export_entry__[] = { __func__, #js_name } + +/** + * Injects constants into JS. These constants are made accessible via + * 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; + +/** + * 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. + */ +- (void)batchDidComplete; + +@end diff --git a/ReactKit/Base/RCTConvert.h b/ReactKit/Base/RCTConvert.h index efa35674ea..ed7327017c 100644 --- a/ReactKit/Base/RCTConvert.h +++ b/ReactKit/Base/RCTConvert.h @@ -4,7 +4,13 @@ #import #import "Layout.h" +#import "RCTPointerEvents.h" +/** + * This class provides a collection of conversion functions for mapping + * JSON objects to native types and classes. These are useful when writing + * custom RCTViewManager setter methods. + */ @interface RCTConvert : NSObject + (BOOL)BOOL:(id)json; @@ -57,4 +63,21 @@ + (css_position_type_t)css_position_type_t:(id)json; + (css_wrap_type_t)css_wrap_type_t:(id)json; ++ (RCTPointerEvents)RCTPointerEvents:(id)json; + @end + +/** + * This function will attempt to set a property using a json value by first + * inferring the correct type from all available information, and then + * applying an appropriate conversion method. If the property does not + * exist, or the type cannot be inferred, the function will return NO. + */ +BOOL RCTSetProperty(id target, NSString *keypath, id json); + +/** + * This function attempts to copy a property from the source object to the + * destination object using KVC. If the property does not exist, or cannot + * be set, it will do nothing and return NO. + */ +BOOL RCTCopyProperty(id target, id source, NSString *keypath); diff --git a/ReactKit/Base/RCTConvert.m b/ReactKit/Base/RCTConvert.m index cd91d0e6c4..c77bef3fd8 100644 --- a/ReactKit/Base/RCTConvert.m +++ b/ReactKit/Base/RCTConvert.m @@ -4,6 +4,7 @@ #import #import +#import #import "RCTLog.h" @@ -41,7 +42,7 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter]) return default; \ } \ if ([json isKindOfClass:[NSNumber class]]) { \ - if ([[mapping allValues] containsObject:json]) { \ + if ([[mapping allValues] containsObject:json] || [json getter] == default) { \ return [json getter]; \ } \ RCTLogMustFix(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allValues]); \ @@ -651,4 +652,253 @@ RCT_ENUM_CONVERTER(css_wrap_type_t, (@{ @"nowrap": @(CSS_NOWRAP) }), CSS_NOWRAP, intValue) +RCT_ENUM_CONVERTER(RCTPointerEvents, (@{ + @"none": @(RCTPointerEventsNone), + @"boxonly": @(RCTPointerEventsBoxOnly), + @"boxnone": @(RCTPointerEventsBoxNone) +}), RCTPointerEventsUnspecified, integerValue) + @end + +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 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; +} diff --git a/ReactKit/Base/RCTEventDispatcher.h b/ReactKit/Base/RCTEventDispatcher.h index d06994959f..4cfc0d94a9 100644 --- a/ReactKit/Base/RCTEventDispatcher.h +++ b/ReactKit/Base/RCTEventDispatcher.h @@ -4,13 +4,6 @@ @class RCTBridge; -typedef NS_ENUM(NSInteger, RCTTouchEventType) { - RCTTouchEventTypeStart, - RCTTouchEventTypeMove, - RCTTouchEventTypeEnd, - RCTTouchEventTypeCancel -}; - typedef NS_ENUM(NSInteger, RCTTextEventType) { RCTTextEventTypeFocus, RCTTextEventTypeBlur, @@ -39,13 +32,6 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { */ - (void)sendEventWithName:(NSString *)name body:(NSDictionary *)body; -/** - * Send an array of touch events - */ -- (void)sendTouchEventWithType:(RCTTouchEventType)type - touches:(NSArray *)touches - changedIndexes:(NSArray *)changedIndexes; - /** * Send text events */ diff --git a/ReactKit/Base/RCTEventDispatcher.m b/ReactKit/Base/RCTEventDispatcher.m index fec3e32ae5..339873358e 100644 --- a/ReactKit/Base/RCTEventDispatcher.m +++ b/ReactKit/Base/RCTEventDispatcher.m @@ -2,6 +2,7 @@ #import "RCTEventDispatcher.h" +#import "RCTAssert.h" #import "RCTBridge.h" #import "UIView+ReactKit.h" @@ -27,34 +28,6 @@ args:@[body[@"target"], name, body]]; } -/** - * 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. - */ -- (void)sendTouchEventWithType:(RCTTouchEventType)type - touches:(NSArray *)touches - changedIndexes:(NSArray *)changedIndexes -{ - static NSString *events[] = { - @"topTouchStart", - @"topTouchMove", - @"topTouchEnd", - @"topTouchCancel", - }; - - RCTAssert(touches.count, @"No touches in touchEventArgsForOrderedTouches"); - - [_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches" - args:@[events[type], touches, changedIndexes]]; -} - - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag text:(NSString *)text diff --git a/ReactKit/Base/RCTExport.h b/ReactKit/Base/RCTExport.h deleted file mode 100644 index d4bd1772b1..0000000000 --- a/ReactKit/Base/RCTExport.h +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#import - -#import "RCTLog.h" - -@class RCTBridge; -@class RCTSparseArray; -@class RCTUIManager; - -typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *viewRegistry); - -@class RCTEventDispatcher; -@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 RCTCopyProperty(id target, id source, NSString *keypath); -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 -@optional - -/** - * Optional initializer for modules that require access - * to bridge features, such as sending events or making JS calls - */ -- (instancetype)initWithBridge:(RCTBridge *)bridge; - -/** - * 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. Note that this method is not inherited when you - * subclass a module, and you should not call [super constantsToExport] when - * implementing it. - */ -+ (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. - */ -- (void)batchDidComplete; - -@end - - -/** - * Provides minimal interface needed to register a UIViewManager module - */ -@protocol RCTNativeViewModule - -/** - * 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. - */ -- (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 - -/** - * The module name exposed to JS. If omitted, this will be inferred - * automatically by using the view module's class name. - */ -+ (NSString *)moduleName; - -/** - * This method instantiates a shadow view to be managed by the module. If omitted, - * 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; - -/** - * 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_:(id)property - * forView:(UIView *)view - * withDefaultView:(UIView *)defaultView; - * - * - (void)set_:(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" - * } - * } - * } - * - * 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; - -/** - * 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" - * } - * } - * - * 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; - -/** - * Injects constants into JS. These constants are made accessible via - * 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; - -/** - * To deprecate, hopefully - */ -- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry; - -@end diff --git a/ReactKit/Base/RCTExport.m b/ReactKit/Base/RCTExport.m deleted file mode 100644 index be206cba4b..0000000000 --- a/ReactKit/Base/RCTExport.m +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#import "RCTExport.h" - -#import -#import -#import -#import -#import -#import - -#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 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 - 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); -} diff --git a/ReactKit/Base/RCTJavaScriptAppEngine.h b/ReactKit/Base/RCTJavaScriptAppEngine.h deleted file mode 100755 index 0f13034240..0000000000 --- a/ReactKit/Base/RCTJavaScriptAppEngine.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#import - -#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 - -- (instancetype)initWithBridge:(RCTBridge *)bridge; -- (void)loadBundleAtURL:(NSURL *)moduleURL useCache:(BOOL)useCache onComplete:(RCTJavaScriptCompleteBlock)onComplete; -+ (void)resetCacheForBundleAtURL:(NSURL *)moduleURL; - -@end diff --git a/ReactKit/Base/RCTJavaScriptAppEngine.m b/ReactKit/Base/RCTJavaScriptAppEngine.m deleted file mode 100755 index bf1172457a..0000000000 --- a/ReactKit/Base/RCTJavaScriptAppEngine.m +++ /dev/null @@ -1,158 +0,0 @@ -// 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 -{ - NSDictionary *_loadedResource; - __weak RCTBridge *_bridge; -} - -- (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; - } - return self; -} - -#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 diff --git a/ReactKit/Base/RCTLog.h b/ReactKit/Base/RCTLog.h index 7d75dd8af5..a97d134584 100644 --- a/ReactKit/Base/RCTLog.h +++ b/ReactKit/Base/RCTLog.h @@ -20,28 +20,28 @@ // 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; \ +#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) { \ + __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]; \ - } \ + [__RCTLog__redBox showErrorMessage:__RCTLog__msg]; \ + } \ } while (0) #define RCTLog(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__) @@ -53,71 +53,13 @@ #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); +NSArray *_RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5); +NSString *_RCTLogFormatString(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5); #ifdef __cplusplus } diff --git a/ReactKit/Base/RCTLog.m b/ReactKit/Base/RCTLog.m index e32585fe57..dcf60084e9 100644 --- a/ReactKit/Base/RCTLog.m +++ b/ReactKit/Base/RCTLog.m @@ -10,6 +10,16 @@ void RCTInjectLogFunction(RCTLogFunction func) { injectedLogFunction = func; } +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]; +} + // TODO (#5906496): // kinda ugly that this is tied to RCTBridge NSString *_RCTLogObjects(NSArray *objects, NSString *level) { @@ -31,3 +41,48 @@ NSString *_RCTLogObjects(NSArray *objects, NSString *level) } return str; } + +// 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. +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); + [objects addObject:obj ?: @"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; +} + +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]; +} diff --git a/ReactKit/Base/RCTModuleMethod.h b/ReactKit/Base/RCTModuleMethod.h deleted file mode 100644 index 1095f8d471..0000000000 --- a/ReactKit/Base/RCTModuleMethod.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#import - -@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 diff --git a/ReactKit/Base/RCTModuleMethod.m b/ReactKit/Base/RCTModuleMethod.m deleted file mode 100644 index 1cc9d312d1..0000000000 --- a/ReactKit/Base/RCTModuleMethod.m +++ /dev/null @@ -1,35 +0,0 @@ -// 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 diff --git a/ReactKit/Base/RCTPointerEvents.h b/ReactKit/Base/RCTPointerEvents.h new file mode 100644 index 0000000000..522bcce4c4 --- /dev/null +++ b/ReactKit/Base/RCTPointerEvents.h @@ -0,0 +1,10 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +typedef NS_ENUM(NSInteger, RCTPointerEvents) { + RCTPointerEventsUnspecified = 0, // Default + RCTPointerEventsNone, + RCTPointerEventsBoxNone, + RCTPointerEventsBoxOnly, +}; diff --git a/ReactKit/Base/RCTRootView.h b/ReactKit/Base/RCTRootView.h index d5712e8d35..f0c3816386 100644 --- a/ReactKit/Base/RCTRootView.h +++ b/ReactKit/Base/RCTRootView.h @@ -2,14 +2,35 @@ #import -@protocol RCTJavaScriptExecutor; - @interface RCTRootView : UIView +/** + * The URL of the bundled application script (required). + * Setting this will clear the view contents, and trigger + * an asynchronous load/download and execution of the script. + */ @property (nonatomic, strong) NSURL *scriptURL; + +/** + * The name of the JavaScript module to execute within the + * specified scriptURL (required). Setting this will not have + * any immediate effect, but it must be done prior to loading + * the script. + */ @property (nonatomic, copy) NSString *moduleName; + +/** + * The default properties to apply to the view when the script bundle + * is first loaded. Defaults to nil/empty. + */ @property (nonatomic, copy) NSDictionary *initialProperties; -@property (nonatomic, strong) id executor; + +/** + * The class of the RCTJavaScriptExecutor to use with this view. + * If not specified, it will default to using RCTContextExecutor. + * Changes will take effect next time the bundle is reloaded. + */ +@property (nonatomic, strong) Class executorClass; /** * Reload this root view, or all root views, respectively. diff --git a/ReactKit/Base/RCTRootView.m b/ReactKit/Base/RCTRootView.m index a6ec650385..f5227ffe3e 100644 --- a/ReactKit/Base/RCTRootView.m +++ b/ReactKit/Base/RCTRootView.m @@ -5,25 +5,25 @@ #import "RCTBridge.h" #import "RCTContextExecutor.h" #import "RCTEventDispatcher.h" -#import "RCTJavaScriptAppEngine.h" +#import "RCTKeyCommands.h" +#import "RCTLog.h" +#import "RCTRedBox.h" #import "RCTTouchHandler.h" #import "RCTUIManager.h" #import "RCTUtils.h" -#import "RCTViewManager.h" #import "RCTWebViewExecutor.h" #import "UIView+ReactKit.h" -#import "RCTKeyCommands.h" NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification"; @implementation RCTRootView { RCTBridge *_bridge; - RCTJavaScriptAppEngine *_appEngine; RCTTouchHandler *_touchHandler; + id _executor; } -static BOOL _useWebExec; +static Class _globalExecutorClass; + (void)initialize { @@ -36,11 +36,12 @@ static BOOL _useWebExec; 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; + _globalExecutorClass = [RCTWebViewExecutor class]; [self reloadAll]; }]; @@ -50,21 +51,18 @@ static BOOL _useWebExec; - (id)initWithCoder:(NSCoder *)aDecoder { - self = [super initWithCoder:aDecoder]; - if (!self) return nil; - - [self setUp]; - + if ((self = [super initWithCoder:aDecoder])) { + [self setUp]; + } return self; } - (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (!self) return nil; - - [self setUp]; - + if ((self = [super initWithFrame:frame])) { + self.backgroundColor = [UIColor whiteColor]; + [self setUp]; + } return self; } @@ -81,7 +79,6 @@ static BOOL _useWebExec; selector:@selector(reload) name:RCTRootViewReloadNotification object:nil]; - self.backgroundColor = [UIColor whiteColor]; } - (void)dealloc @@ -104,10 +101,9 @@ static BOOL _useWebExec; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ - @"rootTag": self.reactTag ?: @0, + @"rootTag": self.reactTag, @"initialProps": self.initialProperties ?: @{}, }; - [_bridge enqueueJSCall:@"Bundler.runApplication" args:@[moduleName, appParameters]]; } @@ -122,28 +118,81 @@ static BOOL _useWebExec; return; } - __weak typeof(self) weakSelf = self; - RCTJavaScriptCompleteBlock callback = ^(NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf bundleFinishedLoading:error]; - }); - }; - + // Clean up + [self removeGestureRecognizer:_touchHandler]; [_executor invalidate]; [_bridge invalidate]; - if (!_useWebExec) { - _executor = [[RCTContextExecutor alloc] init]; - } else { - _executor = [[RCTWebViewExecutor alloc] init]; - } - + // Choose local executor if specified, followed by global, followed by default + _executor = [[_executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class] alloc] init]; _bridge = [[RCTBridge alloc] initWithJavaScriptExecutor:_executor]; + _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; + [self addGestureRecognizer:_touchHandler]; - _appEngine = [[RCTJavaScriptAppEngine alloc] initWithBridge:_bridge]; - _touchHandler = [[RCTTouchHandler alloc] initWithEventDispatcher:_bridge.eventDispatcher rootView:self]; + // Load the bundle + NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:_scriptURL completionHandler: + ^(NSData *data, NSURLResponse *response, NSError *error) { + + // Handle general request errors + if (error) { + if ([[error domain] isEqualToString:NSURLErrorDomain]) { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: @"Could not connect to development server. Ensure node server is running - run 'npm start' from ReactKit root", + NSLocalizedFailureReasonErrorKey: [error localizedDescription], + NSUnderlyingErrorKey: error, + }; + error = [NSError errorWithDomain:@"JSServer" + code:error.code + userInfo:userInfo]; + } + [self bundleFinishedLoading:error]; + return; + } + + // Parse response as text + 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]; + + // Handle HTTP errors + 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}; + } + error = [NSError errorWithDomain:@"JSServer" + code:[(NSHTTPURLResponse *)response statusCode] + userInfo:userInfo]; + + [self bundleFinishedLoading:error]; + return; + } - [_appEngine loadBundleAtURL:_scriptURL useCache:NO onComplete:callback]; + // Success! + [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^(NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self bundleFinishedLoading:error]; + }); + }]; + + }]; + + [task resume]; } - (void)setScriptURL:(NSURL *)scriptURL @@ -156,12 +205,6 @@ static BOOL _useWebExec; [self loadBundle]; } -- (void)setExecutor:(id)executor -{ - RCTAssert(!_bridge, @"You may only change the Javascript Executor prior to loading a script bundle."); - _executor = executor; -} - - (BOOL)isReactRootView { return YES; @@ -169,7 +212,6 @@ static BOOL _useWebExec; - (void)reload { - [RCTJavaScriptAppEngine resetCacheForBundleAtURL:_scriptURL]; [self loadBundle]; } diff --git a/ReactKit/Base/RCTTouchHandler.h b/ReactKit/Base/RCTTouchHandler.h index 8019ade46e..e75bb15313 100644 --- a/ReactKit/Base/RCTTouchHandler.h +++ b/ReactKit/Base/RCTTouchHandler.h @@ -2,11 +2,10 @@ #import -@class RCTEventDispatcher; +@class RCTBridge; @interface RCTTouchHandler : UIGestureRecognizer -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher - rootView:(UIView *)rootView; +- (instancetype)initWithBridge:(RCTBridge *)bridge; @end diff --git a/ReactKit/Base/RCTTouchHandler.m b/ReactKit/Base/RCTTouchHandler.m index 8b4c43e7c1..cc5cdee7c0 100644 --- a/ReactKit/Base/RCTTouchHandler.m +++ b/ReactKit/Base/RCTTouchHandler.m @@ -5,7 +5,7 @@ #import #import "RCTAssert.h" -#import "RCTEventDispatcher.h" +#import "RCTBridge.h" #import "RCTLog.h" #import "RCTUIManager.h" #import "RCTUtils.h" @@ -16,8 +16,7 @@ @implementation RCTTouchHandler { - __weak UIView *_rootView; - RCTEventDispatcher *_eventDispatcher; + __weak RCTBridge *_bridge; /** * Arrays managed in parallel tracking native touch object along with the @@ -35,16 +34,13 @@ RCT_NOT_DESIGNATED_INITIALIZER(); } -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher - rootView:(UIView *)rootView +- (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithTarget:nil action:NULL])) { - RCTAssert(eventDispatcher != nil, @"Expect an event dispatcher"); - RCTAssert(rootView != nil, @"Expect a root view"); + RCTAssert(bridge != nil, @"Expect an event dispatcher"); - _eventDispatcher = eventDispatcher; - _rootView = rootView; + _bridge = bridge; _nativeTouches = [[NSMutableOrderedSet alloc] init]; _reactTouches = [[NSMutableArray alloc] init]; @@ -53,11 +49,17 @@ // `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. self.cancelsTouchesInView = NO; - [_rootView addGestureRecognizer:self]; } return self; } +typedef NS_ENUM(NSInteger, RCTTouchEventType) { + RCTTouchEventTypeStart, + RCTTouchEventTypeMove, + RCTTouchEventTypeEnd, + RCTTouchEventTypeCancel +}; + #pragma mark - Bookkeeping for touch indices - (void)_recordNewTouches:(NSSet *)touches @@ -122,7 +124,7 @@ { UITouch *nativeTouch = _nativeTouches[touchIndex]; CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window]; - CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:_rootView]; + CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:self.view]; UIView *touchView = _touchViews[touchIndex]; CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView]; @@ -135,15 +137,26 @@ reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS } -- (void)_updateAndDispatchTouches:(NSSet *)touches eventType:(RCTTouchEventType)eventType +/** + * 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. + */ +- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName { // Update touches - NSMutableArray *changedIndices = [[NSMutableArray alloc] init]; + NSMutableArray *changedIndexes = [[NSMutableArray alloc] init]; for (UITouch *touch in touches) { NSInteger index = [_nativeTouches indexOfObject:touch]; RCTAssert(index != NSNotFound, @"Touch not found. This is a critical bug."); [self _updateReactTouchAtIndex:index]; - [changedIndices addObject:@(index)]; + [changedIndexes addObject:@(index)]; } // Deep copy the touches because they will be accessed from another thread @@ -154,9 +167,8 @@ } // Dispatch touch event - [_eventDispatcher sendTouchEventWithType:eventType - touches:reactTouches - changedIndexes:changedIndices]; + [_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches" + args:@[eventName, reactTouches, changedIndexes]]; } #pragma mark - Gesture Recognizer Delegate Callbacks @@ -169,7 +181,7 @@ // "start" has to record new touches before extracting the event. // "end"/"cancel" needs to remove the touch *after* extracting the event. [self _recordNewTouches:touches]; - [self _updateAndDispatchTouches:touches eventType:RCTTouchEventTypeStart]; + [self _updateAndDispatchTouches:touches eventName:@"topTouchStart"]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event @@ -178,20 +190,20 @@ if (self.state == UIGestureRecognizerStateFailed) { return; } - [self _updateAndDispatchTouches:touches eventType:RCTTouchEventTypeMove]; + [self _updateAndDispatchTouches:touches eventName:@"topTouchMove"]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; - [self _updateAndDispatchTouches:touches eventType:RCTTouchEventTypeEnd]; + [self _updateAndDispatchTouches:touches eventName:@"topTouchEnd"]; [self _recordRemovedTouches:touches]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; - [self _updateAndDispatchTouches:touches eventType:RCTTouchEventTypeCancel]; + [self _updateAndDispatchTouches:touches eventName:@"topTouchCancel"]; [self _recordRemovedTouches:touches]; } diff --git a/ReactKit/Base/RCTUtils.h b/ReactKit/Base/RCTUtils.h index 3011d5bebe..3612b1f274 100644 --- a/ReactKit/Base/RCTUtils.h +++ b/ReactKit/Base/RCTUtils.h @@ -17,7 +17,7 @@ do { \ NSString *RCTJSONStringify(id jsonObject, NSError **error); id RCTJSONParse(NSString *jsonString, NSError **error); -// Get MD5 hash of a string +// Get MD5 hash of a string (TODO: currently unused. Remove?) NSString *RCTMD5Hash(NSString *string); // Get screen metrics in a thread-safe way diff --git a/ReactKit/Base/RCTViewNodeProtocol.h b/ReactKit/Base/RCTViewNodeProtocol.h index 9d682de290..d45806f263 100644 --- a/ReactKit/Base/RCTViewNodeProtocol.h +++ b/ReactKit/Base/RCTViewNodeProtocol.h @@ -22,8 +22,4 @@ // 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 diff --git a/ReactKit/Executors/RCTWebViewExecutor.h b/ReactKit/Executors/RCTWebViewExecutor.h index 1ce8184791..7f695f3f39 100644 --- a/ReactKit/Executors/RCTWebViewExecutor.h +++ b/ReactKit/Executors/RCTWebViewExecutor.h @@ -13,15 +13,16 @@ * environment. And ensure main thread operations are actually added to a queue * instead of being executed immediately if already on the main thread. */ -@interface RCTWebViewExecutor : NSObject - -@property (nonatomic, readwrite, strong) UIWebView *webView; +@interface RCTWebViewExecutor : NSObject // Only one callback stored - will only be invoked for the latest issued // application script request. -@property (nonatomic, readwrite, copy) RCTJavaScriptCompleteBlock onApplicationScriptLoaded; +@property (nonatomic, copy) RCTJavaScriptCompleteBlock onApplicationScriptLoaded; -- (instancetype)initWithWebView:(UIWebView *)webView; +/** + * Instantiate with a specific webview instance + */ +- (instancetype)initWithWebView:(UIWebView *)webView NS_DESIGNATED_INITIALIZER; /** * Invoke this to reclaim the web view for reuse. This is necessary in order to diff --git a/ReactKit/Executors/RCTWebViewExecutor.m b/ReactKit/Executors/RCTWebViewExecutor.m index e774089bda..3d76760e59 100644 --- a/ReactKit/Executors/RCTWebViewExecutor.m +++ b/ReactKit/Executors/RCTWebViewExecutor.m @@ -23,8 +23,13 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) va_end(args); } +@interface RCTWebViewExecutor () + +@end + @implementation RCTWebViewExecutor { + UIWebView *_webView; NSMutableDictionary *_objectsToInject; } diff --git a/ReactKit/Modules/RCTAlertManager.h b/ReactKit/Modules/RCTAlertManager.h index a3c3026c7f..e24e0bc95c 100644 --- a/ReactKit/Modules/RCTAlertManager.h +++ b/ReactKit/Modules/RCTAlertManager.h @@ -2,8 +2,8 @@ #import -#import "RCTExport.h" +#import "RCTBridgeModule.h" -@interface RCTAlertManager : NSObject +@interface RCTAlertManager : NSObject @end diff --git a/ReactKit/Modules/RCTDataManager.h b/ReactKit/Modules/RCTDataManager.h index 854f08c577..47f80ba1ce 100644 --- a/ReactKit/Modules/RCTDataManager.h +++ b/ReactKit/Modules/RCTDataManager.h @@ -2,9 +2,9 @@ #import -#import "RCTExport.h" +#import "RCTBridgeModule.h" -@interface RCTDataManager : NSObject +@interface RCTDataManager : NSObject @end diff --git a/ReactKit/Modules/RCTExceptionsManager.h b/ReactKit/Modules/RCTExceptionsManager.h index 05a9d77725..02ea332026 100644 --- a/ReactKit/Modules/RCTExceptionsManager.h +++ b/ReactKit/Modules/RCTExceptionsManager.h @@ -2,8 +2,8 @@ #import -#import "RCTExport.h" +#import "RCTBridgeModule.h" -@interface RCTExceptionsManager : NSObject +@interface RCTExceptionsManager : NSObject @end diff --git a/ReactKit/Modules/RCTTiming.h b/ReactKit/Modules/RCTTiming.h index c7b7fcc35b..aa55c25213 100644 --- a/ReactKit/Modules/RCTTiming.h +++ b/ReactKit/Modules/RCTTiming.h @@ -2,13 +2,9 @@ #import -#import "RCTExport.h" +#import "RCTBridgeModule.h" #import "RCTInvalidating.h" -@class RCTBridge; - -@interface RCTTiming : NSObject - -- (instancetype)initWithBridge:(RCTBridge *)bridge; +@interface RCTTiming : NSObject @end diff --git a/ReactKit/Modules/RCTUIManager.h b/ReactKit/Modules/RCTUIManager.h index 3b13e69f1b..96ea15d28f 100644 --- a/ReactKit/Modules/RCTUIManager.h +++ b/ReactKit/Modules/RCTUIManager.h @@ -2,19 +2,16 @@ #import -#import "RCTExport.h" +#import "RCTBridgeModule.h" #import "RCTInvalidating.h" -@class RCTBridge; @class RCTRootView; @class RCTShadowView; +@class RCTSparseArray; @protocol RCTScrollableProtocol; -@protocol RCTViewNodeProtocol; -@interface RCTUIManager : NSObject - -- (instancetype)initWithBridge:(RCTBridge *)bridge; +@interface RCTUIManager : NSObject @property (nonatomic, strong) RCTSparseArray *shadowViewRegistry; @property (nonatomic, strong) RCTSparseArray *viewRegistry; @@ -26,9 +23,6 @@ */ @property (nonatomic, readwrite, weak) id nativeMainScrollDelegate; -+ (UIView *)closestReactAncestor:(UIView *)view; -+ (UIView *)closestReactAncestorThatRespondsToTouch:(UITouch *)touch; - - (void)registerRootView:(RCTRootView *)rootView; + (UIView *)JSResponder; diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index 75d58a65df..63a22d223f 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -19,7 +19,7 @@ #import "RCTUtils.h" #import "RCTView.h" #import "RCTViewNodeProtocol.h" -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" #import "UIView+ReactKit.h" @class RCTAnimationConfig; @@ -52,17 +52,20 @@ static NSDictionary *RCTViewModuleClasses(void) continue; } - if (![cls conformsToProtocol:@protocol(RCTNativeViewModule)]) { - // Not an RCTNativeModule + if (![cls isSubclassOfClass:[RCTViewManager class]]) { + // Not a view module continue; } // Get module name - NSString *moduleName = [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(cls); + NSString *moduleName = [cls moduleName]; // Check module name is unique id existingClass = modules[moduleName]; - RCTCAssert(existingClass == Nil, @"Attempted to register RCTNativeViewModule class %@ for the name '%@', but name was already registered by class %@", cls, moduleName, existingClass); + RCTCAssert(existingClass == Nil, @"Attempted to register view module class %@ " + "for the name '%@', but name was already registered by class %@", cls, moduleName, existingClass); + + // Add to module list modules[moduleName] = cls; } @@ -90,14 +93,13 @@ static NSDictionary *RCTViewModuleClasses(void) __weak RCTBridge *_bridge; } -- (id )_managerInstanceForViewWithModuleName:(NSString *)moduleName +- (RCTViewManager *)_managerInstanceForViewWithModuleName:(NSString *)moduleName { - id managerInstance = _viewManagers[moduleName]; + RCTViewManager *managerInstance = _viewManagers[moduleName]; if (managerInstance == nil) { RCTLogWarn(@"No manager class found for view with module name \"%@\"", moduleName); - managerInstance = [[RCTUIViewManager alloc] init]; + managerInstance = [[RCTViewManager alloc] init]; } - return managerInstance; } @@ -183,27 +185,6 @@ static NSDictionary *RCTViewModuleClasses(void) }); } -+ (UIView *)closestReactAncestor:(UIView *)view -{ - UIView *currentUIView = view; - while (currentUIView && !currentUIView.reactTag) { - currentUIView = currentUIView.superview; - } - return (UIView *)currentUIView; -} - -+ (UIView *)closestReactAncestorThatRespondsToTouch:(UITouch *)touch -{ - UIView *currentUIView = [RCTUIManager closestReactAncestor:touch.view]; - while (currentUIView != nil) { - if ([currentUIView isUserInteractionEnabled]) { // TODO: implement respondsToTouch:touch mechanism - return currentUIView; - } - currentUIView = [RCTUIManager closestReactAncestor:currentUIView.superview]; - } - return nil; -} - /** * Unregisters views from registries */ @@ -212,8 +193,8 @@ static NSDictionary *RCTViewModuleClasses(void) for (id child in children) { RCTTraverseViewNodes(registry[child.reactTag], ^(id subview) { RCTAssert(![subview isReactRootView], @"Host views should not be unregistered"); - if ([subview respondsToSelector:@selector(reactWillDestroy)]) { - [subview reactWillDestroy]; + if ([subview conformsToProtocol:@protocol(RCTInvalidating)]) { + [(id)subview invalidate]; } registry[subview.reactTag] = nil; }); @@ -332,17 +313,10 @@ static NSDictionary *RCTViewModuleClasses(void) { NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1]; [topView collectUpdatedProperties:applierBlocks parentProperties:@{}]; - NSMutableArray *propsAppliers = [NSMutableArray arrayWithCapacity:applierBlocks.count]; - - for (RCTApplierBlock propsApplier in applierBlocks) { - [propsAppliers addObject:propsApplier]; - } - - NSArray *immutablePropsAppliers = propsAppliers; + [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { - for (NSUInteger f = 0; f < immutablePropsAppliers.count; f++) { - RCTApplierBlock applier = [immutablePropsAppliers objectAtIndex: f]; - applier(viewRegistry); + for (RCTApplierBlock block in applierBlocks) { + block(viewRegistry); } }]; } @@ -515,7 +489,7 @@ static NSDictionary *RCTViewModuleClasses(void) } } -static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, id manager) +static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, RCTViewManager *manager) { // TODO: cache respondsToSelector tests if ([manager respondsToSelector:setter]) { @@ -531,7 +505,7 @@ static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, } static void RCTSetViewProps(NSDictionary *props, UIView *view, - UIView *defaultView, id manager) + UIView *defaultView, RCTViewManager *manager) { [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { @@ -544,7 +518,7 @@ static void RCTSetViewProps(NSDictionary *props, UIView *view, } static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView, - RCTShadowView *defaultView, id manager) + RCTShadowView *defaultView, RCTViewManager *manager) { [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { @@ -575,14 +549,14 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView { RCT_EXPORT(createView); - id manager = [self _managerInstanceForViewWithModuleName:moduleName]; + RCTViewManager *manager = [self _managerInstanceForViewWithModuleName:moduleName]; // Generate default view, used for resetting default props if (!_defaultShadowViews[moduleName]) { - _defaultShadowViews[moduleName] = ([manager respondsToSelector:@selector(shadowView)] ? [manager shadowView] : nil) ?: [[RCTShadowView alloc] init]; + _defaultShadowViews[moduleName] = [manager shadowView]; } - RCTShadowView *shadowView = ([manager respondsToSelector:@selector(shadowView)] ? [manager shadowView] : nil) ?: [[RCTShadowView alloc] init]; + RCTShadowView *shadowView = [manager shadowView]; shadowView.moduleName = moduleName; shadowView.reactTag = reactTag; RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], manager); @@ -619,7 +593,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCT_EXPORT(); RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; - id manager = [self _managerInstanceForViewWithModuleName:moduleName]; + RCTViewManager *manager = [self _managerInstanceForViewWithModuleName:moduleName]; RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], manager); [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { @@ -657,8 +631,8 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView // pending blocks to a new array. This guards against mutation while // processing the pending blocks in another thread. - for (id viewManager in _viewManagers.allValues) { - RCTViewManagerUIBlock uiBlock = [viewManager respondsToSelector:@selector(uiBlockToAmendWithShadowViewRegistry:)] ? [viewManager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry] : nil; + for (RCTViewManager *manager in _viewManagers.allValues) { + RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; if (uiBlock != nil) { [self addUIBlock:uiBlock]; } diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index 036ef258ab..a9a5cb9ad3 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 134FCB371A6D4ED700051CC8 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137029581A6C197000575408 /* RCTRawTextManager.m */; }; 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */; }; 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3C1A6E7F0800051CC8 /* RCTWebViewExecutor.m */; }; - 137029331A69659C00575408 /* RCTExport.m in Sources */ = {isa = PBXBuildFile; fileRef = 830213F41A65574D00B993E6 /* RCTExport.m */; }; 137029491A698FF000575408 /* RCTNetworkImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137029401A698FF000575408 /* RCTNetworkImageViewManager.m */; }; 137029501A6990A100575408 /* RCTNetworkImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1370294F1A6990A100575408 /* RCTNetworkImageView.m */; }; 137029531A69923600575408 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 137029521A69923600575408 /* RCTImageDownloader.m */; }; @@ -37,9 +36,8 @@ 13B080291A694C4900A75B9A /* RCTDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080281A694C4900A75B9A /* RCTDataManager.m */; }; 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067491A70F434002CDEE1 /* RCTUIManager.m */; }; 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */; }; - 13E067561A70F44B002CDEE1 /* RCTUIViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTUIViewManager.m */; }; + 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; }; - 13E067581A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067521A70F44B002CDEE1 /* RCTViewManager.m */; }; 13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; @@ -49,10 +47,8 @@ 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA591A601E9000E9B192 /* RCTRedBox.m */; }; 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 */; }; 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 */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -128,16 +124,14 @@ 13E067491A70F434002CDEE1 /* RCTUIManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManager.m; sourceTree = ""; }; 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowView.h; sourceTree = ""; }; 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowView.m; sourceTree = ""; }; - 13E0674D1A70F44B002CDEE1 /* RCTUIViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIViewManager.h; sourceTree = ""; }; - 13E0674E1A70F44B002CDEE1 /* RCTUIViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIViewManager.m; sourceTree = ""; }; + 13E0674D1A70F44B002CDEE1 /* RCTViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewManager.h; sourceTree = ""; }; + 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTViewManager.m; sourceTree = ""; }; 13E0674F1A70F44B002CDEE1 /* RCTView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTView.h; sourceTree = ""; }; 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; - 13E067511A70F44B002CDEE1 /* RCTViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewManager.h; sourceTree = ""; }; - 13E067521A70F44B002CDEE1 /* RCTViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTViewManager.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+ReactKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+ReactKit.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+ReactKit.m"; sourceTree = ""; }; - 830213F31A654E0800B993E6 /* RCTExport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTExport.h; sourceTree = ""; }; - 830213F41A65574D00B993E6 /* RCTExport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTExport.m; sourceTree = ""; }; + 13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; }; + 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; }; 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; }; 83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSparseArray.h; sourceTree = ""; }; @@ -159,14 +153,10 @@ 83CBBA631A601ECA00E9B192 /* RCTJavaScriptExecutor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptExecutor.h; sourceTree = ""; }; 83CBBA651A601EF300E9B192 /* RCTEventDispatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTEventDispatcher.h; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = ""; }; - 83EEC2EC1A604AB200C39218 /* RCTModuleMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModuleMethod.h; sourceTree = ""; }; - 83EEC2ED1A604AB200C39218 /* RCTModuleMethod.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleMethod.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -224,12 +214,10 @@ 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */, 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */, 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */, - 13E0674D1A70F44B002CDEE1 /* RCTUIViewManager.h */, - 13E0674E1A70F44B002CDEE1 /* RCTUIViewManager.m */, 13E0674F1A70F44B002CDEE1 /* RCTView.h */, 13E067501A70F44B002CDEE1 /* RCTView.m */, - 13E067511A70F44B002CDEE1 /* RCTViewManager.h */, - 13E067521A70F44B002CDEE1 /* RCTViewManager.m */, + 13E0674D1A70F44B002CDEE1 /* RCTViewManager.h */, + 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */, 13B07FF61A6947C200A75B9A /* RCTScrollView.h */, 13B07FF71A6947C200A75B9A /* RCTScrollView.m */, 13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */, @@ -308,17 +296,12 @@ 83CBBA611A601EB200E9B192 /* RCTAutoInsetsProtocol.h */, 83CBBA5E1A601EAA00E9B192 /* RCTBridge.h */, 83CBBA5F1A601EAA00E9B192 /* RCTBridge.m */, - 83EEC2EC1A604AB200C39218 /* RCTModuleMethod.h */, - 83EEC2ED1A604AB200C39218 /* RCTModuleMethod.m */, 83CBBACA1A6023D300E9B192 /* RCTConvert.h */, 83CBBACB1A6023D300E9B192 /* RCTConvert.m */, - 830213F31A654E0800B993E6 /* RCTExport.h */, - 830213F41A65574D00B993E6 /* RCTExport.m */, + 830213F31A654E0800B993E6 /* RCTBridgeModule.h */, 830A229C1A66C68A008503DA /* RCTRootView.h */, 830A229D1A66C68A008503DA /* RCTRootView.m */, 83CBBA4C1A601E3B00E9B192 /* RCTInvalidating.h */, - 83CBBA851A60202500E9B192 /* RCTJavaScriptAppEngine.h */, - 83CBBA861A60202500E9B192 /* RCTJavaScriptAppEngine.m */, 83CBBA651A601EF300E9B192 /* RCTEventDispatcher.h */, 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */, 83CBBA631A601ECA00E9B192 /* RCTJavaScriptExecutor.h */, @@ -336,6 +319,7 @@ 137029511A69923600575408 /* RCTImageDownloader.h */, 137029521A69923600575408 /* RCTImageDownloader.m */, 13B07FCD1A683B5F00A75B9A /* RCTScrollableProtocol.h */, + 13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */, ); path = Base; sourceTree = ""; @@ -424,7 +408,7 @@ 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */, - 13E067561A70F44B002CDEE1 /* RCTUIViewManager.m in Sources */, + 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */, 13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */, @@ -442,17 +426,13 @@ 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */, 137029491A698FF000575408 /* RCTNetworkImageViewManager.m in Sources */, 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */, - 13E067581A70F44B002CDEE1 /* RCTViewManager.m in Sources */, 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */, 83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */, 13B080081A6947C200A75B9A /* RCTShadowText.m in Sources */, 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, - 83CBBA871A60202500E9B192 /* RCTJavaScriptAppEngine.m in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, - 83EEC2EE1A604AB200C39218 /* RCTModuleMethod.m in Sources */, 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */, - 137029331A69659C00575408 /* RCTExport.m in Sources */, 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */, 134FCB371A6D4ED700051CC8 /* RCTRawTextManager.m in Sources */, 13B0800B1A6947C200A75B9A /* RCTTextManager.m in Sources */, diff --git a/ReactKit/Views/RCTNavItemManager.h b/ReactKit/Views/RCTNavItemManager.h index a9136dcc6b..3c2a321057 100644 --- a/ReactKit/Views/RCTNavItemManager.h +++ b/ReactKit/Views/RCTNavItemManager.h @@ -1,8 +1,8 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTNavItemManager : RCTUIViewManager +@interface RCTNavItemManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTNavigator.h b/ReactKit/Views/RCTNavigator.h index 76c6deab92..5d928efa7b 100644 --- a/ReactKit/Views/RCTNavigator.h +++ b/ReactKit/Views/RCTNavigator.h @@ -2,9 +2,11 @@ #import +#import "RCTInvalidating.h" + @class RCTEventDispatcher; -@interface RCTNavigator : UIView +@interface RCTNavigator : UIView @property (nonatomic, strong) UIView *reactNavSuperviewLink; @property (nonatomic, assign) NSInteger requestedTopOfStack; diff --git a/ReactKit/Views/RCTNavigator.m b/ReactKit/Views/RCTNavigator.m index b5769540bb..d03ff7874f 100644 --- a/ReactKit/Views/RCTNavigator.m +++ b/ReactKit/Views/RCTNavigator.m @@ -200,7 +200,7 @@ NSInteger kNeverProgressed = -10000; @end -@interface RCTNavigator() +@interface RCTNavigator() { RCTEventDispatcher *_eventDispatcher; NSInteger _numberOfViewControllerMovesToIgnore; @@ -417,9 +417,14 @@ NSInteger kNeverProgressed = -10000; return _currentViews; } -- (void)reactWillDestroy +- (BOOL)isValid { - // Removes run loop's references to `displayLink`. + return _displayLink != nil; +} + +- (void)invalidate +{ + // Prevent displayLink from retaining the navigator indefinitely [_displayLink invalidate]; _displayLink = nil; _runTimer = nil; diff --git a/ReactKit/Views/RCTNavigatorManager.h b/ReactKit/Views/RCTNavigatorManager.h index a8cb19f14c..d32d21096e 100644 --- a/ReactKit/Views/RCTNavigatorManager.h +++ b/ReactKit/Views/RCTNavigatorManager.h @@ -1,8 +1,8 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTNavigatorManager : RCTUIViewManager +@interface RCTNavigatorManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTNetworkImageViewManager.h b/ReactKit/Views/RCTNetworkImageViewManager.h index 9b999f9792..5b34e60602 100644 --- a/ReactKit/Views/RCTNetworkImageViewManager.h +++ b/ReactKit/Views/RCTNetworkImageViewManager.h @@ -1,8 +1,8 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTNetworkImageViewManager : RCTUIViewManager +@interface RCTNetworkImageViewManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTRawTextManager.h b/ReactKit/Views/RCTRawTextManager.h index a0adc08653..4c67fd3285 100644 --- a/ReactKit/Views/RCTRawTextManager.h +++ b/ReactKit/Views/RCTRawTextManager.h @@ -1,7 +1,7 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTRawTextManager : RCTUIViewManager +@interface RCTRawTextManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTScrollView.m b/ReactKit/Views/RCTScrollView.m index 157cd406da..d017c7edcf 100644 --- a/ReactKit/Views/RCTScrollView.m +++ b/ReactKit/Views/RCTScrollView.m @@ -242,11 +242,6 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; return frame; } -- (void)insertReactSubview:(NSObject *)subview atIndex:(NSInteger)atIndex -{ - [super insertReactSubview:subview atIndex:atIndex]; -} - @end @implementation RCTScrollView diff --git a/ReactKit/Views/RCTScrollViewManager.h b/ReactKit/Views/RCTScrollViewManager.h index 1e053725b0..835e1c3226 100644 --- a/ReactKit/Views/RCTScrollViewManager.h +++ b/ReactKit/Views/RCTScrollViewManager.h @@ -1,8 +1,8 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTScrollViewManager : RCTUIViewManager +@interface RCTScrollViewManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTStaticImageManager.h b/ReactKit/Views/RCTStaticImageManager.h index e2e317a83a..ab89cb96b8 100644 --- a/ReactKit/Views/RCTStaticImageManager.h +++ b/ReactKit/Views/RCTStaticImageManager.h @@ -1,7 +1,7 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTStaticImageManager : RCTUIViewManager +@interface RCTStaticImageManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTStaticImageManager.m b/ReactKit/Views/RCTStaticImageManager.m index f4f86abb57..2cd08e1e27 100644 --- a/ReactKit/Views/RCTStaticImageManager.m +++ b/ReactKit/Views/RCTStaticImageManager.m @@ -14,12 +14,13 @@ return [[RCTStaticImage alloc] init]; } +RCT_EXPORT_VIEW_PROPERTY(capInsets) 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) { + if ([[[json description] pathExtension] caseInsensitiveCompare:@"gif"] == NSOrderedSame) { [view.layer addAnimation:[RCTConvert GIF:json] forKey:@"contents"]; } else { view.image = [RCTConvert UIImage:json]; @@ -29,11 +30,6 @@ RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode) } } -- (void)set_capInsets:(id)json forView:(RCTStaticImage *)view withDefaultView:(RCTStaticImage *)defaultView -{ - view.capInsets = json ? [RCTConvert UIEdgeInsets:json] : defaultView.capInsets; -} - - (void)set_tintColor:(id)json forView:(RCTStaticImage *)view withDefaultView:(RCTStaticImage *)defaultView { if (json) { diff --git a/ReactKit/Views/RCTTextFieldManager.h b/ReactKit/Views/RCTTextFieldManager.h index c8e7e24658..1f83a47d8f 100644 --- a/ReactKit/Views/RCTTextFieldManager.h +++ b/ReactKit/Views/RCTTextFieldManager.h @@ -1,8 +1,8 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTTextFieldManager : RCTUIViewManager +@interface RCTTextFieldManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTTextManager.h b/ReactKit/Views/RCTTextManager.h index d1e2883d5c..0359cce409 100644 --- a/ReactKit/Views/RCTTextManager.h +++ b/ReactKit/Views/RCTTextManager.h @@ -1,8 +1,8 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTTextManager : RCTUIViewManager +@interface RCTTextManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTTextManager.m b/ReactKit/Views/RCTTextManager.m index f7f096dba5..2673e60fe1 100644 --- a/ReactKit/Views/RCTTextManager.m +++ b/ReactKit/Views/RCTTextManager.m @@ -67,6 +67,13 @@ RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment); shadowView.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; } +// TODO: the purpose of this block is effectively just to copy properties from the shadow views +// to their equivalent UIViews. In this case, the property being copied is the attributed text, +// but the same principle could be used to copy any property. The implementation is really ugly tho +// because the RCTViewManager doesn't retain a reference to the views that it manages, so it basically +// has to search the entire view hierarchy for relevant views. Not awesome. This seems like something +// where we could introduce a generic solution - perhaps a method on RCTShadowView that is called after +// layout to copy its properties across? - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry { NSMutableArray *shadowBlocks = [NSMutableArray new]; diff --git a/ReactKit/Views/RCTUIActivityIndicatorViewManager.h b/ReactKit/Views/RCTUIActivityIndicatorViewManager.h index 8aa9a22f58..d676613590 100644 --- a/ReactKit/Views/RCTUIActivityIndicatorViewManager.h +++ b/ReactKit/Views/RCTUIActivityIndicatorViewManager.h @@ -1,7 +1,7 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import "RCTViewManager.h" -@interface RCTUIActivityIndicatorViewManager : RCTUIViewManager +@interface RCTUIActivityIndicatorViewManager : RCTViewManager @end diff --git a/ReactKit/Views/RCTUIViewManager.h b/ReactKit/Views/RCTUIViewManager.h deleted file mode 100644 index acec4c78a4..0000000000 --- a/ReactKit/Views/RCTUIViewManager.h +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#import - -#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 deleted file mode 100644 index b2dc053b39..0000000000 --- a/ReactKit/Views/RCTUIViewManager.m +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#import "RCTUIViewManager.h" - -#import "RCTConvert.h" -#import "RCTEventDispatcher.h" -#import "RCTLog.h" -#import "RCTShadowView.h" -#import "RCTView.h" - -@implementation RCTUIViewManager -{ - __weak 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]; -} - -// View properties - -RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel) -RCT_EXPORT_VIEW_PROPERTY(hidden) -RCT_EXPORT_VIEW_PROPERTY(backgroundColor) -RCT_REMAP_VIEW_PROPERTY(accessible, isAccessibilityElement) -RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier) -RCT_REMAP_VIEW_PROPERTY(opacity, alpha) -RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor); -RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset); -RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity) -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, layer.transform) - -- (void)set_overflow:(id)json - forView:(UIView *)view - withDefaultView:(UIView *)defaultView -{ - view.clipsToBounds = json ? ![RCTConvert css_overflow:json] : defaultView.clipsToBounds; -} - -- (void)set_pointerEvents:(id)json - forView:(UIView *)view - withDefaultView:(UIView *)defaultView -{ - if (!json) { - view.userInteractionEnabled = defaultView.userInteractionEnabled; - return; - } - - switch ([RCTConvert NSInteger:json]) { - case RCTPointerEventsUnspecified: - // Pointer events "unspecified" acts as if a stylesheet had not specified, - // which is different than "auto" in CSS (which cannot and will not be - // supported in `ReactKit`. "auto" may override a parent's "none". - // Unspecified values do not. - // This wouldn't override a container view's `userInteractionEnabled = NO` - view.userInteractionEnabled = YES; - case RCTPointerEventsNone: - view.userInteractionEnabled = NO; - break; - default: - RCTLogError(@"UIView base class does not support pointerEvent value: %@", json); - } -} - -// ShadowView properties - -- (void)set_backgroundColor:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView -{ - shadowView.backgroundColor = json ? [RCTConvert UIColor:json] : defaultView.backgroundColor; - shadowView.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; -} - -- (void)set_flexDirection:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView -{ - shadowView.flexDirection = json? [RCTConvert css_flex_direction_t:json] : defaultView.flexDirection; -} - -- (void)set_flexWrap:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView -{ - shadowView.flexWrap = json ? [RCTConvert css_wrap_type_t:json] : defaultView.flexWrap; -} - -- (void)set_justifyContent:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView -{ - shadowView.justifyContent = json ? [RCTConvert css_justify_t:json] : defaultView.justifyContent; -} - -- (void)set_alignItems:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView -{ - shadowView.alignItems = json ? [RCTConvert css_align_t:json] : defaultView.alignItems; -} - -- (void)set_alignSelf:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView -{ - shadowView.alignSelf = json ? [RCTConvert css_align_t:json] : defaultView.alignSelf; -} - -- (void)set_position:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView -{ - shadowView.positionType = json ? [RCTConvert css_position_type_t:json] : defaultView.positionType; -} - -@end diff --git a/ReactKit/Views/RCTView.h b/ReactKit/Views/RCTView.h index 76fc6ae25a..77dee1df8c 100644 --- a/ReactKit/Views/RCTView.h +++ b/ReactKit/Views/RCTView.h @@ -4,19 +4,13 @@ #import -// TODO: rehome this -typedef NS_ENUM(NSInteger, RCTPointerEventsValue) { - RCTPointerEventsUnspecified = 0, // Default - RCTPointerEventsNone, - RCTPointerEventsBoxNone, - RCTPointerEventsBoxOnly, -}; +#import "RCTPointerEvents.h" @protocol RCTAutoInsetsProtocol; @interface RCTView : UIView -@property (nonatomic, assign) RCTPointerEventsValue pointerEvents; +@property (nonatomic, assign) RCTPointerEvents pointerEvents; @property (nonatomic, copy) NSString *overrideAccessibilityLabel; + (void)autoAdjustInsetsForView:(UIView *)parentView diff --git a/ReactKit/Views/RCTView.m b/ReactKit/Views/RCTView.m index 1591bb5655..abe6f00d97 100644 --- a/ReactKit/Views/RCTView.m +++ b/ReactKit/Views/RCTView.m @@ -40,7 +40,7 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) return RCTRecursiveAccessibilityLabel(self); } -- (void)setPointerEvents:(RCTPointerEventsValue)pointerEvents +- (void)setPointerEvents:(RCTPointerEvents)pointerEvents { _pointerEvents = pointerEvents; self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone); diff --git a/ReactKit/Views/RCTViewManager.h b/ReactKit/Views/RCTViewManager.h index 96340d38db..e77be09c12 100644 --- a/ReactKit/Views/RCTViewManager.h +++ b/ReactKit/Views/RCTViewManager.h @@ -1,7 +1,148 @@ // Copyright 2004-present Facebook. All Rights Reserved. -#import "RCTUIViewManager.h" +#import -@interface RCTViewManager : RCTUIViewManager +#import "RCTConvert.h" +#import "RCTLog.h" + +@class RCTEventDispatcher; +@class RCTShadowView; +@class RCTSparseArray; +@class RCTUIManager; + +typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *viewRegistry); + +@interface RCTViewManager : NSObject + +/** + * Designated initializer for view modules. Override this when subclassing. + */ +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +/** + * The event dispatcher is used to send events back to the JavaScript application. + * It can either be used directly by the module, or passed on to instantiated + * view subclasses so that they can handle their own events. + */ +@property (nonatomic, readonly, weak) RCTEventDispatcher *eventDispatcher; + +/** + * The module name exposed to React JS. If omitted, this will be inferred + * automatically by using the view module's class name. It is better to not + * override this, and just follow standard naming conventions for your view + * module subclasses. + */ ++ (NSString *)moduleName; + +/** + * This method instantiates a native view to be managed by the module. Override + * this to return a custom view instance, which may be preconfigured with default + * properties, subviews, etc. This 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; + +/** + * This method instantiates a shadow view to be managed by the module. If omitted, + * an ordinary RCTShadowView instance will be created, which is typically fine for + * most view types. As with the -view method, the -shadowView method should return + * a fresh instance each time it is called. + */ +- (RCTShadowView *)shadowView; + +/** + * 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" + * } + * } + * } + * + * Note that this method is not inherited when you subclass a view module, and + * you should not call [super customBubblingEventTypes] when overriding it. + */ ++ (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" + * } + * } + * + * Note that this method is not inherited when you subclass a view module, and + * you should not call [super customDirectEventTypes] when overriding it. + */ ++ (NSDictionary *)customDirectEventTypes; + +/** + * Injects constants into JS. These constants are made accessible via + * NativeModules.moduleName.X. Note that this method is not inherited when you + * subclass a view module, and you should not call [super constantsToExport] + * when overriding it. + */ ++ (NSDictionary *)constantsToExport; + +/** + * To deprecate, hopefully + */ +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry; + +/** + * 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_:(id)property + * forView:(UIView *)view + * withDefaultView:(UIView *)defaultView; + * + * - (void)set_:(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 && !RCTSetProperty(view, @#keypath, json)) || \ + (!json && !RCTCopyProperty(view, defaultView, @#keypath))) { \ + RCTLogMustFix(@"%@ does not have setter for `%s` property", [view class], #name); \ + } \ +} + +/** + * 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 {} @end diff --git a/ReactKit/Views/RCTViewManager.m b/ReactKit/Views/RCTViewManager.m index 7add6d617f..84ca149910 100644 --- a/ReactKit/Views/RCTViewManager.m +++ b/ReactKit/Views/RCTViewManager.m @@ -2,15 +2,169 @@ #import "RCTViewManager.h" +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTLog.h" +#import "RCTShadowView.h" #import "RCTView.h" @implementation RCTViewManager +- (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 [[RCTView alloc] init]; } -RCT_EXPORT_VIEW_PROPERTY(pointerEvents) +- (RCTShadowView *)shadowView +{ + return [[RCTShadowView alloc] init]; +} + ++ (NSDictionary *)customBubblingEventTypes +{ + return nil; +} + ++ (NSDictionary *)customDirectEventTypes +{ + return nil; +} + ++ (NSDictionary *)constantsToExport +{ + return nil; +} + +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry +{ + return nil; +} + +// View properties + +RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel) +RCT_EXPORT_VIEW_PROPERTY(hidden) +RCT_EXPORT_VIEW_PROPERTY(backgroundColor) +RCT_REMAP_VIEW_PROPERTY(accessible, isAccessibilityElement) +RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier) +RCT_REMAP_VIEW_PROPERTY(opacity, alpha) +RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor); +RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset); +RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity) +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) + +- (void)set_overflow:(id)json + forView:(UIView *)view + withDefaultView:(UIView *)defaultView +{ + view.clipsToBounds = json ? ![RCTConvert css_overflow:json] : defaultView.clipsToBounds; +} + +- (void)set_pointerEvents:(id)json + forView:(UIView *)view + withDefaultView:(UIView *)defaultView +{ + if ([view respondsToSelector:@selector(setPointerEvents:)]) { + [(id)view setPointerEvents:json ? [RCTConvert RCTPointerEvents:json] : [(id)defaultView pointerEvents]]; + return; + } + + if (!json) { + view.userInteractionEnabled = defaultView.userInteractionEnabled; + return; + } + + switch ([RCTConvert NSInteger:json]) { + case RCTPointerEventsUnspecified: + // Pointer events "unspecified" acts as if a stylesheet had not specified, + // which is different than "auto" in CSS (which cannot and will not be + // supported in `ReactKit`. "auto" may override a parent's "none". + // Unspecified values do not. + // This wouldn't override a container view's `userInteractionEnabled = NO` + view.userInteractionEnabled = YES; + case RCTPointerEventsNone: + view.userInteractionEnabled = NO; + break; + default: + RCTLogError(@"UIView base class does not support pointerEvent value: %@", json); + } +} + +// ShadowView properties + +- (void)set_backgroundColor:(id)json + forShadowView:(RCTShadowView *)shadowView + withDefaultView:(RCTShadowView *)defaultView +{ + shadowView.backgroundColor = json ? [RCTConvert UIColor:json] : defaultView.backgroundColor; + shadowView.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; +} + +- (void)set_flexDirection:(id)json + forShadowView:(RCTShadowView *)shadowView + withDefaultView:(RCTShadowView *)defaultView +{ + shadowView.flexDirection = json? [RCTConvert css_flex_direction_t:json] : defaultView.flexDirection; +} + +- (void)set_flexWrap:(id)json + forShadowView:(RCTShadowView *)shadowView + withDefaultView:(RCTShadowView *)defaultView +{ + shadowView.flexWrap = json ? [RCTConvert css_wrap_type_t:json] : defaultView.flexWrap; +} + +- (void)set_justifyContent:(id)json + forShadowView:(RCTShadowView *)shadowView + withDefaultView:(RCTShadowView *)defaultView +{ + shadowView.justifyContent = json ? [RCTConvert css_justify_t:json] : defaultView.justifyContent; +} + +- (void)set_alignItems:(id)json + forShadowView:(RCTShadowView *)shadowView + withDefaultView:(RCTShadowView *)defaultView +{ + shadowView.alignItems = json ? [RCTConvert css_align_t:json] : defaultView.alignItems; +} + +- (void)set_alignSelf:(id)json + forShadowView:(RCTShadowView *)shadowView + withDefaultView:(RCTShadowView *)defaultView +{ + shadowView.alignSelf = json ? [RCTConvert css_align_t:json] : defaultView.alignSelf; +} + +- (void)set_position:(id)json + forShadowView:(RCTShadowView *)shadowView + withDefaultView:(RCTShadowView *)defaultView +{ + shadowView.positionType = json ? [RCTConvert css_position_type_t:json] : defaultView.positionType; +} @end