diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index da4350e4c268d4..146034f7cc8639 100644 --- a/Libraries/Components/MapView/MapView.js +++ b/Libraries/Components/MapView/MapView.js @@ -277,7 +277,9 @@ if (Platform.OS === 'android') { uiViewClassName: 'RCTMap', }); } else { - var RCTMap = requireNativeComponent('RCTMap', MapView); + var RCTMap = requireNativeComponent('RCTMap', MapView, { + nativeOnly: {onChange: true, onPress: true} + }); } module.exports = MapView; diff --git a/Libraries/Components/SliderIOS/SliderIOS.ios.js b/Libraries/Components/SliderIOS/SliderIOS.ios.js index bfb2b92714c768..0c06fbf6f84e8c 100644 --- a/Libraries/Components/SliderIOS/SliderIOS.ios.js +++ b/Libraries/Components/SliderIOS/SliderIOS.ios.js @@ -107,6 +107,8 @@ var styles = StyleSheet.create({ }, }); -var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS); +var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS, { + nativeOnly: { onChange: true }, +}); module.exports = SliderIOS; diff --git a/Libraries/Components/SwitchIOS/SwitchIOS.ios.js b/Libraries/Components/SwitchIOS/SwitchIOS.ios.js index 5a56e36b753d4f..e99d512101eb2d 100644 --- a/Libraries/Components/SwitchIOS/SwitchIOS.ios.js +++ b/Libraries/Components/SwitchIOS/SwitchIOS.ios.js @@ -108,6 +108,8 @@ var styles = StyleSheet.create({ }, }); -var RCTSwitch = requireNativeComponent('RCTSwitch', SwitchIOS); +var RCTSwitch = requireNativeComponent('RCTSwitch', SwitchIOS, { + nativeOnly: { onChange: true } +}); module.exports = SwitchIOS; diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 5bb0349ccd885c..a3d457bfdb6f2d 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -226,7 +226,13 @@ var WebView = React.createClass({ }, }); -var RCTWebView = requireNativeComponent('RCTWebView', WebView); +var RCTWebView = requireNativeComponent('RCTWebView', WebView, { + nativeOnly: { + onLoadingStart: true, + onLoadingError: true, + onLoadingFinish: true, + }, +}); var styles = StyleSheet.create({ container: { diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index ddbd23d774df34..db67ed44aeb065 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -21,11 +21,11 @@ @interface RCTImageView () -@property (nonatomic, assign) BOOL onLoadStart; -@property (nonatomic, assign) BOOL onProgress; -@property (nonatomic, assign) BOOL onError; -@property (nonatomic, assign) BOOL onLoad; -@property (nonatomic, assign) BOOL onLoadEnd; +@property (nonatomic, copy) RCTDirectEventBlock onLoadStart; +@property (nonatomic, copy) RCTDirectEventBlock onProgress; +@property (nonatomic, copy) RCTDirectEventBlock onError; +@property (nonatomic, copy) RCTDirectEventBlock onLoad; +@property (nonatomic, copy) RCTDirectEventBlock onLoadEnd; @end @@ -116,19 +116,16 @@ - (void)reloadImage if (_src && !CGSizeEqualToSize(self.frame.size, CGSizeZero)) { if (_onLoadStart) { - NSDictionary *event = @{ @"target": self.reactTag }; - [_bridge.eventDispatcher sendInputEventWithName:@"loadStart" body:event]; + _onLoadStart(nil); } RCTImageLoaderProgressBlock progressHandler = nil; if (_onProgress) { progressHandler = ^(int64_t loaded, int64_t total) { - NSDictionary *event = @{ - @"target": self.reactTag, + _onProgress(@{ @"loaded": @((double)loaded), @"total": @((double)total), - }; - [_bridge.eventDispatcher sendInputEventWithName:@"progress" body:event]; + }); }; } @@ -147,21 +144,15 @@ - (void)reloadImage } if (error) { if (_onError) { - NSDictionary *event = @{ - @"target": self.reactTag, - @"error": error.localizedDescription, - }; - [_bridge.eventDispatcher sendInputEventWithName:@"error" body:event]; + _onError(@{ @"error": error.localizedDescription }); } } else { if (_onLoad) { - NSDictionary *event = @{ @"target": self.reactTag }; - [_bridge.eventDispatcher sendInputEventWithName:@"load" body:event]; + _onLoad(nil); } } if (_onLoadEnd) { - NSDictionary *event = @{ @"target": self.reactTag }; - [_bridge.eventDispatcher sendInputEventWithName:@"loadEnd" body:event]; + _onLoadEnd(nil); } }]; } else { diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m index c797cdfab149f5..bcf2f0bdb4d091 100644 --- a/Libraries/Image/RCTImageViewManager.m +++ b/Libraries/Image/RCTImageViewManager.m @@ -27,11 +27,11 @@ - (UIView *)view RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage) RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) RCT_EXPORT_VIEW_PROPERTY(src, NSString) -RCT_EXPORT_VIEW_PROPERTY(onLoadStart, BOOL) -RCT_EXPORT_VIEW_PROPERTY(onProgress, BOOL) -RCT_EXPORT_VIEW_PROPERTY(onError, BOOL) -RCT_EXPORT_VIEW_PROPERTY(onLoad, BOOL) -RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, BOOL) +RCT_EXPORT_VIEW_PROPERTY(onLoadStart, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, RCTDirectEventBlock) RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView) { if (json) { @@ -43,15 +43,4 @@ - (UIView *)view } } -- (NSArray *)customDirectEventTypes -{ - return @[ - @"loadStart", - @"progress", - @"error", - @"load", - @"loadEnd", - ]; -} - @end diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 9af3a6cebfc1e6..4969b0055ee13e 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -455,6 +455,9 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; [areNew addObject:@(shadowView.isNewView)]; [parentsAreNew addObject:@(shadowView.superview.isNewView)]; + + // TODO (#8214142): this can be greatly simplified by sending the layout + // event directly from the shadow thread, which may be better anyway. id event = (id)kCFNull; if (shadowView.onLayout) { event = @{ @@ -1128,67 +1131,68 @@ static void RCTMeasureLayout(RCTShadowView *view, }]; } -- (NSDictionary *)bubblingEventsConfig +- (NSDictionary *)constantsToExport { - NSMutableDictionary *customBubblingEventTypesConfigs = [NSMutableDictionary new]; - for (RCTComponentData *componentData in _componentDataByName.allValues) { - RCTViewManager *manager = componentData.manager; - if (RCTClassOverridesInstanceMethod([manager class], @selector(customBubblingEventTypes))) { - NSArray *events = [manager customBubblingEventTypes]; - if (RCT_DEBUG) { - RCTAssert(!events || [events isKindOfClass:[NSArray class]], - @"customBubblingEventTypes must return an array, but %@ returned %@", - [manager class], [events class]); - } - for (NSString *eventName in events) { - NSString *topName = RCTNormalizeInputEventName(eventName); - if (!customBubblingEventTypesConfigs[topName]) { - NSString *bubbleName = [topName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"]; - customBubblingEventTypesConfigs[topName] = @{ - @"phasedRegistrationNames": @{ - @"bubbled": bubbleName, - @"captured": [bubbleName stringByAppendingString:@"Capture"], - } - }; - } - } - } - }; + NSMutableDictionary *allJSConstants = [NSMutableDictionary new]; + NSMutableDictionary *directEvents = [NSMutableDictionary new]; + NSMutableDictionary *bubblingEvents = [NSMutableDictionary new]; - return customBubblingEventTypesConfigs; -} + [_componentDataByName enumerateKeysAndObjectsUsingBlock: + ^(NSString *name, RCTComponentData *componentData, __unused BOOL *stop) { -- (NSDictionary *)directEventsConfig -{ - NSMutableDictionary *customDirectEventTypes = [NSMutableDictionary new]; - for (RCTComponentData *componentData in _componentDataByName.allValues) { - RCTViewManager *manager = componentData.manager; - if (RCTClassOverridesInstanceMethod([manager class], @selector(customDirectEventTypes))) { - NSArray *events = [manager customDirectEventTypes]; - if (RCT_DEBUG) { - RCTAssert(!events || [events isKindOfClass:[NSArray class]], - @"customDirectEventTypes must return an array, but %@ returned %@", - [manager class], [events class]); - } - for (NSString *eventName in events) { - NSString *topName = RCTNormalizeInputEventName(eventName); - if (!customDirectEventTypes[topName]) { - customDirectEventTypes[topName] = @{ - @"registrationName": [topName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"], - }; - } - } - } - }; + RCTViewManager *manager = componentData.manager; + NSMutableDictionary *constantsNamespace = + [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; - return customDirectEventTypes; -} + // Add custom constants + // TODO: should these be inherited? + NSDictionary *constants = RCTClassOverridesInstanceMethod([manager class], @selector(constantsToExport)) ? [manager constantsToExport] : nil; + if (constants.count) { + RCTAssert(constantsNamespace[@"Constants"] == nil , @"Cannot redefine Constants in namespace: %@", name); + // add an additional 'Constants' namespace for each class + constantsNamespace[@"Constants"] = constants; + } + + // Add native props + NSDictionary *viewConfig = [componentData viewConfig]; + constantsNamespace[@"NativeProps"] = viewConfig[@"propTypes"]; + + // Add direct events + for (NSString *eventName in viewConfig[@"directEvents"]) { + if (!directEvents[eventName]) { + directEvents[eventName] = @{ + @"registrationName": [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"], + }; + } + if (RCT_DEBUG && bubblingEvents[eventName]) { + RCTLogError(@"Component '%@' re-registered bubbling event '%@' as a " + "direct event", componentData.name, eventName); + } + } + + // Add bubbling events + for (NSString *eventName in viewConfig[@"bubblingEvents"]) { + if (!bubblingEvents[eventName]) { + NSString *bubbleName = [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"]; + bubblingEvents[eventName] = @{ + @"phasedRegistrationNames": @{ + @"bubbled": bubbleName, + @"captured": [bubbleName stringByAppendingString:@"Capture"], + } + }; + } + if (RCT_DEBUG && directEvents[eventName]) { + RCTLogError(@"Component '%@' re-registered direct event '%@' as a " + "bubbling event", componentData.name, eventName); + } + } + + allJSConstants[name] = [constantsNamespace copy]; + }]; -- (NSDictionary *)constantsToExport -{ - NSMutableDictionary *allJSConstants = [@{ - @"customBubblingEventTypes": [self bubblingEventsConfig], - @"customDirectEventTypes": [self directEventsConfig], + [allJSConstants addEntriesFromDictionary:@{ + @"customBubblingEventTypes": bubblingEvents, + @"customDirectEventTypes": directEvents, @"Dimensions": @{ @"window": @{ @"width": @(RCTScreenSize().width), @@ -1200,28 +1204,8 @@ - (NSDictionary *)constantsToExport @"height": @(RCTScreenSize().height), }, }, - } mutableCopy]; - - [_componentDataByName enumerateKeysAndObjectsUsingBlock: - ^(NSString *name, RCTComponentData *componentData, __unused BOOL *stop) { - RCTViewManager *manager = componentData.manager; - NSMutableDictionary *constantsNamespace = - [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; - - // Add custom constants - // TODO: should these be inherited? - NSDictionary *constants = RCTClassOverridesInstanceMethod([manager class], @selector(constantsToExport)) ? [manager constantsToExport] : nil; - if (constants.count) { - RCTAssert(constantsNamespace[@"Constants"] == nil , @"Cannot redefine Constants in namespace: %@", name); - // add an additional 'Constants' namespace for each class - constantsNamespace[@"Constants"] = constants; - } - - // Add native props - constantsNamespace[@"NativeProps"] = [componentData viewConfig]; - - allJSConstants[name] = [constantsNamespace copy]; }]; + return allJSConstants; } diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 97a27835381dbd..ce803446dbb8b5 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; }; 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */; }; 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */; }; + 133CAE8E1B8E5CFD00F6AD92 /* RCTDatePicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 133CAE8D1B8E5CFD00F6AD92 /* RCTDatePicker.m */; }; 13456E931ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */; }; 13456E961ADAD482009F94A7 /* RCTConvert+MapKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */; }; 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */; }; @@ -104,6 +105,8 @@ 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = ""; }; 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = ""; }; 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControlManager.m; sourceTree = ""; }; + 133CAE8C1B8E5CFD00F6AD92 /* RCTDatePicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDatePicker.h; sourceTree = ""; }; + 133CAE8D1B8E5CFD00F6AD92 /* RCTDatePicker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDatePicker.m; sourceTree = ""; }; 13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = ""; }; 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; }; 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewControllerProtocol.h; sourceTree = ""; }; @@ -345,6 +348,8 @@ 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */, 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */, + 133CAE8C1B8E5CFD00F6AD92 /* RCTDatePicker.h */, + 133CAE8D1B8E5CFD00F6AD92 /* RCTDatePicker.m */, 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */, 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */, 14435CE11AAC4AE100FC20F4 /* RCTMap.h */, @@ -597,6 +602,7 @@ 13456E961ADAD482009F94A7 /* RCTConvert+MapKit.m in Sources */, 13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */, 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */, + 133CAE8E1B8E5CFD00F6AD92 /* RCTDatePicker.m in Sources */, 14C2CA761B3AC64F00E6CBB2 /* RCTFrameUpdate.m in Sources */, 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */, 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */, diff --git a/React/Views/RCTComponent.h b/React/Views/RCTComponent.h index bc73ac8d2910a5..215acb1d0cf86e 100644 --- a/React/Views/RCTComponent.h +++ b/React/Views/RCTComponent.h @@ -9,6 +9,13 @@ #import +/** + * These block types can be used for mapping input event handlers from JS to view + * properties. Unlike JS method callbacks, these can be called multiple times. + */ +typedef void (^RCTDirectEventBlock)(NSDictionary *body); +typedef void (^RCTBubblingEventBlock)(NSDictionary *body); + /** * Logical node in a tree of application components. Both `ShadowView` and * `UIView` conforms to this. Allows us to write utilities that reason about diff --git a/React/Views/RCTComponentData.m b/React/Views/RCTComponentData.m index b01b0268ec0da0..c6fd9412787f18 100644 --- a/React/Views/RCTComponentData.m +++ b/React/Views/RCTComponentData.m @@ -13,6 +13,7 @@ #import "RCTBridge.h" #import "RCTShadowView.h" +#import "RCTUtils.h" #import "RCTViewManager.h" typedef void (^RCTPropBlock)(id view, id json); @@ -140,77 +141,95 @@ - (RCTPropBlock)propBlockForKey:(NSString *)name defaultView:(id)defaultView // Build setter block void (^setterBlock)(id target, id source, id json) = nil; - NSMethodSignature *typeSignature = [[RCTConvert class] methodSignatureForSelector:type]; - switch (typeSignature.methodReturnType[0]) { - -#define RCT_CASE(_value, _type) \ - case _value: { \ - _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ - _type (*get)(id, SEL) = (typeof(get))objc_msgSend; \ - void (*set)(id, SEL, _type) = (typeof(set))objc_msgSend; \ - setterBlock = ^(id target, id source, id json) { \ - set(target, setter, json ? convert([RCTConvert class], type, json) : get(source, getter)); \ - }; \ - break; \ - } + if (type == NSSelectorFromString(@"RCTBubblingEventBlock:") || + type == NSSelectorFromString(@"RCTDirectEventBlock:")) { + + // Special case for event handlers + __weak RCTViewManager *weakManager = _manager; + setterBlock = ^(id target, __unused id source, id json) { + __weak id weakTarget = target; + ((void (*)(id, SEL, id))objc_msgSend)(target, setter, [RCTConvert BOOL:json] ? ^(NSDictionary *body) { + body = [NSMutableDictionary dictionaryWithDictionary:body]; + ((NSMutableDictionary *)body)[@"target"] = weakTarget.reactTag; + [weakManager.bridge.eventDispatcher sendInputEventWithName:RCTNormalizeInputEventName(name) body:body]; + } : nil); + }; + + } else { + + // Ordinary property handlers + NSMethodSignature *typeSignature = [[RCTConvert class] methodSignatureForSelector:type]; + switch (typeSignature.methodReturnType[0]) { + + #define RCT_CASE(_value, _type) \ + case _value: { \ + _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ + _type (*get)(id, SEL) = (typeof(get))objc_msgSend; \ + void (*set)(id, SEL, _type) = (typeof(set))objc_msgSend; \ + setterBlock = ^(id target, id source, id json) { \ + set(target, setter, json ? convert([RCTConvert class], type, json) : get(source, getter)); \ + }; \ + break; \ + } + + RCT_CASE(_C_SEL, SEL) + RCT_CASE(_C_CHARPTR, const char *) + RCT_CASE(_C_CHR, char) + RCT_CASE(_C_UCHR, unsigned char) + RCT_CASE(_C_SHT, short) + RCT_CASE(_C_USHT, unsigned short) + RCT_CASE(_C_INT, int) + RCT_CASE(_C_UINT, unsigned int) + RCT_CASE(_C_LNG, long) + RCT_CASE(_C_ULNG, unsigned long) + RCT_CASE(_C_LNG_LNG, long long) + RCT_CASE(_C_ULNG_LNG, unsigned long long) + RCT_CASE(_C_FLT, float) + RCT_CASE(_C_DBL, double) + RCT_CASE(_C_BOOL, BOOL) + RCT_CASE(_C_PTR, void *) + RCT_CASE(_C_ID, id) + + case _C_STRUCT_B: + default: { + + NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature]; + typeInvocation.selector = type; + typeInvocation.target = [RCTConvert class]; + + __block NSInvocation *sourceInvocation = nil; + __block NSInvocation *targetInvocation = nil; + + setterBlock = ^(id target, id source, id json) { \ + + // Get value + void *value = malloc(typeSignature.methodReturnLength); + if (json) { + [typeInvocation setArgument:&json atIndex:2]; + [typeInvocation invoke]; + [typeInvocation getReturnValue:value]; + } else { + if (!sourceInvocation && source) { + NSMethodSignature *signature = [source methodSignatureForSelector:getter]; + sourceInvocation = [NSInvocation invocationWithMethodSignature:signature]; + sourceInvocation.selector = getter; + } + [sourceInvocation invokeWithTarget:source]; + [sourceInvocation getReturnValue:value]; + } - RCT_CASE(_C_SEL, SEL) - RCT_CASE(_C_CHARPTR, const char *) - RCT_CASE(_C_CHR, char) - RCT_CASE(_C_UCHR, unsigned char) - RCT_CASE(_C_SHT, short) - RCT_CASE(_C_USHT, unsigned short) - RCT_CASE(_C_INT, int) - RCT_CASE(_C_UINT, unsigned int) - RCT_CASE(_C_LNG, long) - RCT_CASE(_C_ULNG, unsigned long) - RCT_CASE(_C_LNG_LNG, long long) - RCT_CASE(_C_ULNG_LNG, unsigned long long) - RCT_CASE(_C_FLT, float) - RCT_CASE(_C_DBL, double) - RCT_CASE(_C_BOOL, BOOL) - RCT_CASE(_C_PTR, void *) - RCT_CASE(_C_ID, id) - - case _C_STRUCT_B: - default: { - - NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature]; - typeInvocation.selector = type; - typeInvocation.target = [RCTConvert class]; - - __block NSInvocation *sourceInvocation = nil; - __block NSInvocation *targetInvocation = nil; - - setterBlock = ^(id target, id source, id json) { \ - - // Get value - void *value = malloc(typeSignature.methodReturnLength); - if (json) { - [typeInvocation setArgument:&json atIndex:2]; - [typeInvocation invoke]; - [typeInvocation getReturnValue:value]; - } else { - if (!sourceInvocation && source) { - NSMethodSignature *signature = [source methodSignatureForSelector:getter]; - sourceInvocation = [NSInvocation invocationWithMethodSignature:signature]; - sourceInvocation.selector = getter; + // Set value + if (!targetInvocation && target) { + NSMethodSignature *signature = [target methodSignatureForSelector:setter]; + targetInvocation = [NSInvocation invocationWithMethodSignature:signature]; + targetInvocation.selector = setter; } - [sourceInvocation invokeWithTarget:source]; - [sourceInvocation getReturnValue:value]; - } - - // Set value - if (!targetInvocation && target) { - NSMethodSignature *signature = [target methodSignatureForSelector:setter]; - targetInvocation = [NSInvocation invocationWithMethodSignature:signature]; - targetInvocation.selector = setter; - } - [targetInvocation setArgument:value atIndex:2]; - [targetInvocation invokeWithTarget:target]; - free(value); - }; - break; + [targetInvocation setArgument:value atIndex:2]; + [targetInvocation invokeWithTarget:target]; + free(value); + }; + break; + } } } @@ -292,9 +311,35 @@ - (void)setProps:(NSDictionary *)props forShadowView:(RCTShadowView *)shadowView - (NSDictionary *)viewConfig { Class managerClass = [_manager class]; - NSMutableDictionary *propTypes = [NSMutableDictionary new]; + + NSMutableArray *directEvents = [NSMutableArray new]; + if (RCTClassOverridesInstanceMethod(managerClass, @selector(customDirectEventTypes))) { + NSArray *events = [_manager customDirectEventTypes]; + if (RCT_DEBUG) { + RCTAssert(!events || [events isKindOfClass:[NSArray class]], + @"customDirectEventTypes must return an array, but %@ returned %@", + managerClass, [events class]); + } + for (NSString *event in events) { + [directEvents addObject:RCTNormalizeInputEventName(event)]; + } + } + + NSMutableArray *bubblingEvents = [NSMutableArray new]; + if (RCTClassOverridesInstanceMethod(managerClass, @selector(customBubblingEventTypes))) { + NSArray *events = [_manager customBubblingEventTypes]; + if (RCT_DEBUG) { + RCTAssert(!events || [events isKindOfClass:[NSArray class]], + @"customBubblingEventTypes must return an array, but %@ returned %@", + managerClass, [events class]); + } + for (NSString *event in events) { + [bubblingEvents addObject:RCTNormalizeInputEventName(event)]; + } + } unsigned int count = 0; + NSMutableDictionary *propTypes = [NSMutableDictionary new]; Method *methods = class_copyMethodList(object_getClass(managerClass), &count); for (unsigned int i = 0; i < count; i++) { Method method = methods[i]; @@ -309,13 +354,41 @@ - (NSDictionary *)viewConfig RCTLogError(@"Property '%@' of component '%@' redefined from '%@' " "to '%@'", name, _name, propTypes[name], type); } - propTypes[name] = type; + + if ([type isEqualToString:@"RCTBubblingEventBlock"]) { + [bubblingEvents addObject:RCTNormalizeInputEventName(name)]; + propTypes[name] = @"BOOL"; + } else if ([type isEqualToString:@"RCTDirectEventBlock"]) { + [directEvents addObject:RCTNormalizeInputEventName(name)]; + propTypes[name] = @"BOOL"; + } else { + propTypes[name] = type; + } } } } free(methods); - return propTypes; + if (RCT_DEBUG) { + for (NSString *event in directEvents) { + if ([bubblingEvents containsObject:event]) { + RCTLogError(@"Component '%@' registered '%@' as both a bubbling event " + "and a direct event", _name, event); + } + } + for (NSString *event in bubblingEvents) { + if ([directEvents containsObject:event]) { + RCTLogError(@"Component '%@' registered '%@' as both a bubbling event " + "and a direct event", _name, event); + } + } + } + + return @{ + @"propTypes" : propTypes, + @"directEvents" : directEvents, + @"bubblingEvents" : bubblingEvents, + }; } @end diff --git a/React/Views/RCTDatePicker.h b/React/Views/RCTDatePicker.h new file mode 100644 index 00000000000000..f036651b427edc --- /dev/null +++ b/React/Views/RCTDatePicker.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface RCTDatePicker : UIDatePicker + +@end diff --git a/React/Views/RCTDatePicker.m b/React/Views/RCTDatePicker.m new file mode 100644 index 00000000000000..0e392dcec1e156 --- /dev/null +++ b/React/Views/RCTDatePicker.m @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTDatePicker.h" + +#import "RCTUtils.h" +#import "UIView+React.h" + +@interface RCTDatePicker () + +@property (nonatomic, copy) RCTBubblingEventBlock onChange; + +@end + +@implementation RCTDatePicker + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + [self addTarget:self action:@selector(didChange) + forControlEvents:UIControlEventValueChanged]; + } + return self; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) + +- (void)didChange +{ + if (_onChange) { + _onChange(@{ @"timestamp": @(self.date.timeIntervalSince1970 * 1000.0) }); + } +} + +@end diff --git a/React/Views/RCTDatePickerManager.m b/React/Views/RCTDatePickerManager.m index f2bef39e3143cc..ef9515e5acaeb6 100644 --- a/React/Views/RCTDatePickerManager.m +++ b/React/Views/RCTDatePickerManager.m @@ -10,6 +10,7 @@ #import "RCTDatePickerManager.h" #import "RCTBridge.h" +#import "RCTDatePicker.h" #import "RCTEventDispatcher.h" #import "UIView+React.h" @@ -30,14 +31,7 @@ @implementation RCTDatePickerManager - (UIView *)view { - // TODO: we crash here if the RCTDatePickerManager is released - // while the UIDatePicker is still sending onChange events. To - // fix this we should maybe subclass UIDatePicker and make it - // be its own event target. - UIDatePicker *picker = [UIDatePicker new]; - [picker addTarget:self action:@selector(onChange:) - forControlEvents:UIControlEventValueChanged]; - return picker; + return [RCTDatePicker new]; } RCT_EXPORT_VIEW_PROPERTY(date, NSDate) @@ -47,15 +41,6 @@ - (UIView *)view RCT_REMAP_VIEW_PROPERTY(mode, datePickerMode, UIDatePickerMode) RCT_REMAP_VIEW_PROPERTY(timeZoneOffsetInMinutes, timeZone, NSTimeZone) -- (void)onChange:(UIDatePicker *)sender -{ - NSDictionary *event = @{ - @"target": sender.reactTag, - @"timestamp": @(sender.date.timeIntervalSince1970 * 1000.0) - }; - [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:event]; -} - - (NSDictionary *)constantsToExport { UIDatePicker *view = [UIDatePicker new]; diff --git a/React/Views/RCTMap.h b/React/Views/RCTMap.h index 41cc13a12e56b2..65e6aec947b9c0 100644 --- a/React/Views/RCTMap.h +++ b/React/Views/RCTMap.h @@ -11,13 +11,12 @@ #import #import "RCTConvert+MapKit.h" +#import "RCTComponent.h" extern const CLLocationDegrees RCTMapDefaultSpan; extern const NSTimeInterval RCTMapRegionChangeObserveInterval; extern const CGFloat RCTMapZoomBoundBuffer; -@class RCTEventDispatcher; - @interface RCTMap: MKMapView @property (nonatomic, assign) BOOL followUserLocation; @@ -28,6 +27,9 @@ extern const CGFloat RCTMapZoomBoundBuffer; @property (nonatomic, strong) NSTimer *regionChangeObserveTimer; @property (nonatomic, strong) NSMutableArray *annotationIds; +@property (nonatomic, copy) RCTBubblingEventBlock onChange; +@property (nonatomic, copy) RCTBubblingEventBlock onPress; + - (void)setAnnotations:(RCTPointAnnotationArray *)annotations; @end diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index 130446df08494d..f4ee06f98f3a8c 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -46,6 +46,8 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(mapType, MKMapType) RCT_EXPORT_VIEW_PROPERTY(annotations, RCTPointAnnotationArray) +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) { [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES]; @@ -53,35 +55,27 @@ - (UIView *)view #pragma mark MKMapViewDelegate - - -- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view +- (void)mapView:(RCTMap *)mapView didSelectAnnotationView:(MKAnnotationView *)view { - if (![view.annotation isKindOfClass:[MKUserLocation class]]) { + if (mapView.onPress && [view.annotation isKindOfClass:[RCTPointAnnotation class]]) { RCTPointAnnotation *annotation = (RCTPointAnnotation *)view.annotation; - NSString *title = view.annotation.title ?: @""; - NSString *subtitle = view.annotation.subtitle ?: @""; - - NSDictionary *event = @{ - @"target": mapView.reactTag, - @"action": @"annotation-click", - @"annotation": @{ - @"id": annotation.identifier, - @"title": title, - @"subtitle": subtitle, - @"latitude": @(annotation.coordinate.latitude), - @"longitude": @(annotation.coordinate.longitude) - } - }; - - [self.bridge.eventDispatcher sendInputEventWithName:@"press" body:event]; + mapView.onPress(@{ + @"action": @"annotation-click", + @"annotation": @{ + @"id": annotation.identifier, + @"title": annotation.title ?: @"", + @"subtitle": annotation.subtitle ?: @"", + @"latitude": @(annotation.coordinate.latitude), + @"longitude": @(annotation.coordinate.longitude) + } + }); } } - (MKAnnotationView *)mapView:(__unused MKMapView *)mapView viewForAnnotation:(RCTPointAnnotation *)annotation { - if ([annotation isKindOfClass:[MKUserLocation class]]) { + if (![annotation isKindOfClass:[RCTPointAnnotation class]]) { return nil; } @@ -103,23 +97,20 @@ - (MKAnnotationView *)mapView:(__unused MKMapView *)mapView viewForAnnotation:(R return annotationView; } -- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control +- (void)mapView:(RCTMap *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control { - // Pass to js - RCTPointAnnotation *annotation = (RCTPointAnnotation *)view.annotation; - NSString *side = (control == view.leftCalloutAccessoryView) ? @"left" : @"right"; + if (mapView.onPress) { - NSDictionary *event = @{ - @"target": mapView.reactTag, - @"side": side, + // Pass to js + RCTPointAnnotation *annotation = (RCTPointAnnotation *)view.annotation; + mapView.onPress(@{ + @"side": (control == view.leftCalloutAccessoryView) ? @"left" : @"right", @"action": @"callout-click", @"annotationId": annotation.identifier - }; - - [self.bridge.eventDispatcher sendInputEventWithName:@"press" body:event]; + }); + } } - - (void)mapView:(RCTMap *)mapView didUpdateUserLocation:(MKUserLocation *)location { if (mapView.followUserLocation) { @@ -205,24 +196,24 @@ - (void)_regionChanged:(RCTMap *)mapView - (void)_emitRegionChangeEvent:(RCTMap *)mapView continuous:(BOOL)continuous { - MKCoordinateRegion region = mapView.region; - if (!CLLocationCoordinate2DIsValid(region.center)) { - return; - } - -#define FLUSH_NAN(value) (isnan(value) ? 0 : value) - - NSDictionary *event = @{ - @"target": mapView.reactTag, - @"continuous": @(continuous), - @"region": @{ - @"latitude": @(FLUSH_NAN(region.center.latitude)), - @"longitude": @(FLUSH_NAN(region.center.longitude)), - @"latitudeDelta": @(FLUSH_NAN(region.span.latitudeDelta)), - @"longitudeDelta": @(FLUSH_NAN(region.span.longitudeDelta)), + if (mapView.onChange) { + MKCoordinateRegion region = mapView.region; + if (!CLLocationCoordinate2DIsValid(region.center)) { + return; } - }; - [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:event]; + + #define FLUSH_NAN(value) (isnan(value) ? 0 : value) + + mapView.onChange(@{ + @"continuous": @(continuous), + @"region": @{ + @"latitude": @(FLUSH_NAN(region.center.latitude)), + @"longitude": @(FLUSH_NAN(region.center.longitude)), + @"latitudeDelta": @(FLUSH_NAN(region.span.latitudeDelta)), + @"longitudeDelta": @(FLUSH_NAN(region.span.longitudeDelta)), + } + }); + } } @end diff --git a/React/Views/RCTNavItem.h b/React/Views/RCTNavItem.h index 9b75673c30dc95..418d5c5c97cf72 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -9,6 +9,8 @@ #import +#import "RCTComponent.h" + @interface RCTNavItem : UIView @property (nonatomic, copy) NSString *title; @@ -29,4 +31,7 @@ @property (nonatomic, readonly) UIBarButtonItem *leftButtonItem; @property (nonatomic, readonly) UIBarButtonItem *rightButtonItem; +@property (nonatomic, copy) RCTBubblingEventBlock onNavLeftButtonTap; +@property (nonatomic, copy) RCTBubblingEventBlock onNavRightButtonTap; + @end diff --git a/React/Views/RCTNavItem.m b/React/Views/RCTNavItem.m index f875e6aa55777d..5e4043f8495f2d 100644 --- a/React/Views/RCTNavItem.m +++ b/React/Views/RCTNavItem.m @@ -63,15 +63,18 @@ - (UIBarButtonItem *)leftButtonItem { if (!_leftButtonItem) { if (_leftButtonIcon) { - _leftButtonItem = [[UIBarButtonItem alloc] initWithImage:_leftButtonIcon - style:UIBarButtonItemStylePlain - target:nil - action:nil]; + _leftButtonItem = + [[UIBarButtonItem alloc] initWithImage:_leftButtonIcon + style:UIBarButtonItemStylePlain + target:self + action:@selector(handleNavLeftButtonTapped)]; + } else if (_leftButtonTitle.length) { - _leftButtonItem = [[UIBarButtonItem alloc] initWithTitle:_leftButtonTitle - style:UIBarButtonItemStylePlain - target:nil - action:nil]; + _leftButtonItem = + [[UIBarButtonItem alloc] initWithTitle:_leftButtonTitle + style:UIBarButtonItemStylePlain + target:self + action:@selector(handleNavLeftButtonTapped)]; } else { _leftButtonItem = nil; } @@ -79,6 +82,13 @@ - (UIBarButtonItem *)leftButtonItem return _leftButtonItem; } +- (void)handleNavLeftButtonTapped +{ + if (_onNavLeftButtonTap) { + _onNavLeftButtonTap(nil); + } +} + - (void)setRightButtonTitle:(NSString *)rightButtonTitle { _rightButtonTitle = rightButtonTitle; @@ -95,15 +105,18 @@ - (UIBarButtonItem *)rightButtonItem { if (!_rightButtonItem) { if (_rightButtonIcon) { - _rightButtonItem = [[UIBarButtonItem alloc] initWithImage:_rightButtonIcon - style:UIBarButtonItemStylePlain - target:nil - action:nil]; + _rightButtonItem = + [[UIBarButtonItem alloc] initWithImage:_rightButtonIcon + style:UIBarButtonItemStylePlain + target:self + action:@selector(handleNavRightButtonTapped)]; + } else if (_rightButtonTitle.length) { - _rightButtonItem = [[UIBarButtonItem alloc] initWithTitle:_rightButtonTitle - style:UIBarButtonItemStylePlain - target:nil - action:nil]; + _rightButtonItem = + [[UIBarButtonItem alloc] initWithTitle:_rightButtonTitle + style:UIBarButtonItemStylePlain + target:self + action:@selector(handleNavRightButtonTapped)]; } else { _rightButtonItem = nil; } @@ -111,4 +124,11 @@ - (UIBarButtonItem *)rightButtonItem return _rightButtonItem; } +- (void)handleNavRightButtonTapped +{ + if (_onNavRightButtonTap) { + _onNavRightButtonTap(nil); + } +} + @end diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index f785934af12e66..040b7ffceac590 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -39,4 +39,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(rightButtonIcon, UIImage) RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString) +RCT_EXPORT_VIEW_PROPERTY(onNavLeftButtonTap, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onNavRightButtonTap, RCTBubblingEventBlock) + @end diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index fcbc14225be969..a79fe099298e96 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -193,6 +193,9 @@ - (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigati @interface RCTNavigator() +@property (nonatomic, copy) RCTDirectEventBlock onNavigationProgress; +@property (nonatomic, copy) RCTBubblingEventBlock onNavigationComplete; + @property (nonatomic, assign) NSInteger previousRequestedTopOfStack; // Previous views are only mainted in order to detect incorrect @@ -308,12 +311,13 @@ - (void)didUpdateFrame:(__unused RCTFrameUpdate *)update return; } _mostRecentProgress = nextProgress; - [_bridge.eventDispatcher sendInputEventWithName:@"navigationProgress" body:@{ - @"fromIndex": @(_currentlyTransitioningFrom), - @"toIndex": @(_currentlyTransitioningTo), - @"progress": @(nextProgress), - @"target": self.reactTag - }]; + if (_onNavigationProgress) { + _onNavigationProgress(@{ + @"fromIndex": @(_currentlyTransitioningFrom), + @"toIndex": @(_currentlyTransitioningTo), + @"progress": @(nextProgress), + }); + } } } @@ -416,10 +420,11 @@ - (void)removeReactSubview:(UIView *)subview - (void)handleTopOfStackChanged { - [_bridge.eventDispatcher sendInputEventWithName:@"navigationComplete" body:@{ - @"target":self.reactTag, - @"stackLength":@(_navigationController.viewControllers.count) - }]; + if (_onNavigationComplete) { + _onNavigationComplete(@{ + @"stackLength":@(_navigationController.viewControllers.count) + }); + } } - (void)dispatchFakeScrollEvent @@ -502,7 +507,7 @@ - (void)reactBridgeDidFinishTransaction if (jsGettingAhead) { if (reactPushOne) { UIView *lastView = _currentViews.lastObject; - RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_bridge.eventDispatcher]; + RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView]; vc.navigationListener = self; _numberOfViewControllerMovesToIgnore = 1; [_navigationController pushViewController:vc animated:(currentReactCount > 1)]; diff --git a/React/Views/RCTNavigatorManager.m b/React/Views/RCTNavigatorManager.m index 2627d0f647a6e8..0e900a14c81b3c 100644 --- a/React/Views/RCTNavigatorManager.m +++ b/React/Views/RCTNavigatorManager.m @@ -25,22 +25,8 @@ - (UIView *)view } RCT_EXPORT_VIEW_PROPERTY(requestedTopOfStack, NSInteger) - -- (NSArray *)customBubblingEventTypes -{ - return @[ - @"navigationComplete", - @"navLeftButtonTap", - @"navRightButtonTap", - ]; -} - -- (NSArray *)customDirectEventTypes -{ - return @[ - @"navigationProgress", - ]; -} +RCT_EXPORT_VIEW_PROPERTY(onNavigationProgress, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onNavigationComplete, RCTBubblingEventBlock) // TODO: remove error callbacks RCT_EXPORT_METHOD(requestSchedulingJavaScriptNavigation:(nonnull NSNumber *)reactTag diff --git a/React/Views/RCTPicker.h b/React/Views/RCTPicker.h index 9f6a486de2e38c..704fe75876ee71 100644 --- a/React/Views/RCTPicker.h +++ b/React/Views/RCTPicker.h @@ -9,13 +9,6 @@ #import -@class RCTEventDispatcher; - @interface RCTPicker : UIPickerView -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - -@property (nonatomic, copy) NSArray *items; -@property (nonatomic, assign) NSInteger selectedIndex; - @end diff --git a/React/Views/RCTPicker.m b/React/Views/RCTPicker.m index 09c7247d278710..1f256cc484bc85 100644 --- a/React/Views/RCTPicker.m +++ b/React/Views/RCTPicker.m @@ -9,37 +9,28 @@ #import "RCTPicker.h" -#import "RCTConvert.h" -#import "RCTEventDispatcher.h" #import "RCTUtils.h" #import "UIView+React.h" -const NSInteger UNINITIALIZED_INDEX = -1; - @interface RCTPicker() +@property (nonatomic, copy) NSArray *items; +@property (nonatomic, assign) NSInteger selectedIndex; +@property (nonatomic, copy) RCTBubblingEventBlock onChange; + @end @implementation RCTPicker -{ - RCTEventDispatcher *_eventDispatcher; - NSArray *_items; - NSInteger _selectedIndex; -} -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithFrame:(CGRect)frame { - RCTAssertParam(eventDispatcher); - - if ((self = [super initWithFrame:CGRectZero])) { - _eventDispatcher = eventDispatcher; - _selectedIndex = UNINITIALIZED_INDEX; + if ((self = [super initWithFrame:frame])) { + _selectedIndex = NSNotFound; self.delegate = self; } return self; } -RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)setItems:(NSArray *)items @@ -51,7 +42,7 @@ - (void)setItems:(NSArray *)items - (void)setSelectedIndex:(NSInteger)selectedIndex { if (_selectedIndex != selectedIndex) { - BOOL animated = _selectedIndex != UNINITIALIZED_INDEX; // Don't animate the initial value + BOOL animated = _selectedIndex != NSNotFound; // Don't animate the initial value _selectedIndex = selectedIndex; dispatch_async(dispatch_get_main_queue(), ^{ [self selectRow:selectedIndex inComponent:0 animated:animated]; @@ -94,13 +85,12 @@ - (void)pickerView:(__unused UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(__unused NSInteger)component { _selectedIndex = row; - NSDictionary *event = @{ - @"target": self.reactTag, - @"newIndex": @(row), - @"newValue": [self valueForRow:row] - }; - - [_eventDispatcher sendInputEventWithName:@"change" body:event]; + if (_onChange) { + _onChange(@{ + @"newIndex": @(row), + @"newValue": [self valueForRow:row] + }); + } } @end diff --git a/React/Views/RCTPickerManager.m b/React/Views/RCTPickerManager.m index 6de76bcb72cbec..8d1e18120a4118 100644 --- a/React/Views/RCTPickerManager.m +++ b/React/Views/RCTPickerManager.m @@ -19,7 +19,7 @@ @implementation RCTPickerManager - (UIView *)view { - return [[RCTPicker alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; + return [RCTPicker new]; } RCT_EXPORT_VIEW_PROPERTY(items, NSDictionaryArray) diff --git a/React/Views/RCTSegmentedControl.h b/React/Views/RCTSegmentedControl.h index 3e95735bd31f75..500a236e9dc92f 100644 --- a/React/Views/RCTSegmentedControl.h +++ b/React/Views/RCTSegmentedControl.h @@ -9,13 +9,12 @@ #import -@class RCTEventDispatcher; +#import "RCTComponent.h" @interface RCTSegmentedControl : UISegmentedControl -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - @property (nonatomic, copy) NSArray *values; @property (nonatomic, assign) NSInteger selectedIndex; +@property (nonatomic, copy) RCTBubblingEventBlock onChange; @end diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m index 1b28bacc79c98b..543de9c4f2d012 100644 --- a/React/Views/RCTSegmentedControl.m +++ b/React/Views/RCTSegmentedControl.m @@ -14,17 +14,13 @@ #import "UIView+React.h" @implementation RCTSegmentedControl -{ - RCTEventDispatcher *_eventDispatcher; -} -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithFrame:(CGRect)frame { - if ((self = [super initWithFrame:CGRectZero])) { - _eventDispatcher = eventDispatcher; + if ((self = [super initWithFrame:frame])) { _selectedIndex = self.selectedSegmentIndex; - [self addTarget:self action:@selector(onChange:) - forControlEvents:UIControlEventValueChanged]; + [self addTarget:self action:@selector(didChange) + forControlEvents:UIControlEventValueChanged]; } return self; } @@ -45,14 +41,14 @@ - (void)setSelectedIndex:(NSInteger)selectedIndex super.selectedSegmentIndex = selectedIndex; } -- (void)onChange:(UISegmentedControl *)sender +- (void)didChange { - NSDictionary *event = @{ - @"target": self.reactTag, - @"value": [self titleForSegmentAtIndex:sender.selectedSegmentIndex], - @"selectedSegmentIndex": @(sender.selectedSegmentIndex) - }; - [_eventDispatcher sendInputEventWithName:@"change" body:event]; + if (_onChange) { + _onChange(@{ + @"value": [self titleForSegmentAtIndex:_selectedIndex], + @"selectedSegmentIndex": @(_selectedIndex) + }); + } } @end diff --git a/React/Views/RCTSegmentedControlManager.m b/React/Views/RCTSegmentedControlManager.m index 1c5621e7a32038..cbe9f4a4b2be71 100644 --- a/React/Views/RCTSegmentedControlManager.m +++ b/React/Views/RCTSegmentedControlManager.m @@ -19,7 +19,7 @@ @implementation RCTSegmentedControlManager - (UIView *)view { - return [[RCTSegmentedControl alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; + return [RCTSegmentedControl new]; } RCT_EXPORT_VIEW_PROPERTY(values, NSStringArray) @@ -27,6 +27,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(momentary, BOOL) RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) - (NSDictionary *)constantsToExport { diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 7211b17a7d28cf..9aa7f029959148 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -39,7 +39,7 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *viewRegistry); @property (nonatomic, copy) NSString *viewName; @property (nonatomic, strong) UIColor *backgroundColor; // Used to propagate to children @property (nonatomic, assign) RCTUpdateLifecycle layoutLifecycle; -@property (nonatomic, assign) BOOL onLayout; +@property (nonatomic, copy) RCTDirectEventBlock onLayout; /** * isNewView - Used to track the first time the view is introduced into the hierarchy. It is initialized YES, then is diff --git a/React/Views/RCTSlider.h b/React/Views/RCTSlider.h index 916419a29d3c90..664b0689b435af 100644 --- a/React/Views/RCTSlider.h +++ b/React/Views/RCTSlider.h @@ -9,6 +9,10 @@ #import +#import "RCTComponent.h" + @interface RCTSlider : UISlider +@property (nonatomic, copy) RCTBubblingEventBlock onChange; + @end diff --git a/React/Views/RCTSliderManager.m b/React/Views/RCTSliderManager.m index 9ce7699d1b1db4..694e8855bd085a 100644 --- a/React/Views/RCTSliderManager.m +++ b/React/Views/RCTSliderManager.m @@ -27,25 +27,24 @@ - (UIView *)view return slider; } -static void RCTSendSliderEvent(RCTSliderManager *self, UISlider *sender, BOOL continuous) +static void RCTSendSliderEvent(RCTSlider *sender, BOOL continuous) { - NSDictionary *event = @{ - @"target": sender.reactTag, - @"value": @(sender.value), - @"continuous": @(continuous), - }; - - [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:event]; + if (sender.onChange) { + sender.onChange(@{ + @"value": @(sender.value), + @"continuous": @(continuous), + }); + } } -- (void)sliderValueChanged:(UISlider *)sender +- (void)sliderValueChanged:(RCTSlider *)sender { - RCTSendSliderEvent(self, sender, YES); + RCTSendSliderEvent(sender, YES); } -- (void)sliderTouchEnd:(UISlider *)sender +- (void)sliderTouchEnd:(RCTSlider *)sender { - RCTSendSliderEvent(self, sender, NO); + RCTSendSliderEvent(sender, NO); } RCT_EXPORT_VIEW_PROPERTY(value, float); @@ -53,5 +52,6 @@ - (void)sliderTouchEnd:(UISlider *)sender RCT_EXPORT_VIEW_PROPERTY(maximumValue, float); RCT_EXPORT_VIEW_PROPERTY(minimumTrackTintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(maximumTrackTintColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock); @end diff --git a/React/Views/RCTSwitch.h b/React/Views/RCTSwitch.h index 5c01abf49b0c1f..f94bd5052198d9 100644 --- a/React/Views/RCTSwitch.h +++ b/React/Views/RCTSwitch.h @@ -10,8 +10,11 @@ #import +#import "RCTComponent.h" + @interface RCTSwitch : UISwitch @property (nonatomic, assign) BOOL wasOn; +@property (nonatomic, copy) RCTBubblingEventBlock onChange; @end diff --git a/React/Views/RCTSwitchManager.m b/React/Views/RCTSwitchManager.m index 42a54210c4b53e..7fe065baa82866 100644 --- a/React/Views/RCTSwitchManager.m +++ b/React/Views/RCTSwitchManager.m @@ -30,11 +30,9 @@ - (UIView *)view - (void)onChange:(RCTSwitch *)sender { if (sender.wasOn != sender.on) { - [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:@{ - @"target": sender.reactTag, - @"value": @(sender.on) - }]; - + if (sender.onChange) { + sender.onChange(@{ @"value": @(sender.on) }); + } sender.wasOn = sender.on; } } @@ -43,6 +41,7 @@ - (void)onChange:(RCTSwitch *)sender RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(thumbTintColor, UIColor); RCT_REMAP_VIEW_PROPERTY(value, on, BOOL); +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock); RCT_CUSTOM_VIEW_PROPERTY(disabled, BOOL, RCTSwitch) { if (json) { diff --git a/React/Views/RCTTabBar.h b/React/Views/RCTTabBar.h index 5c24b903929223..ba0bbd7e68bb4a 100644 --- a/React/Views/RCTTabBar.h +++ b/React/Views/RCTTabBar.h @@ -9,14 +9,10 @@ #import -@class RCTEventDispatcher; - @interface RCTTabBar : UIView @property (nonatomic, strong) UIColor *tintColor; @property (nonatomic, strong) UIColor *barTintColor; @property (nonatomic, assign) BOOL translucent; -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - @end diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 75e017a98bc3f1..13d60f113578dc 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -25,17 +25,13 @@ @interface RCTTabBar() @implementation RCTTabBar { BOOL _tabsChanged; - RCTEventDispatcher *_eventDispatcher; UITabBarController *_tabController; NSMutableArray *_tabViews; } -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithFrame:(CGRect)frame { - RCTAssertParam(eventDispatcher); - - if ((self = [super initWithFrame:CGRectZero])) { - _eventDispatcher = eventDispatcher; + if ((self = [super initWithFrame:frame])) { _tabViews = [NSMutableArray new]; _tabController = [UITabBarController new]; _tabController.delegate = self; @@ -44,7 +40,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher return self; } -RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (UIViewController *)reactViewController @@ -100,8 +95,7 @@ - (void)reactBridgeDidFinishTransaction for (RCTTabBarItem *tab in [self reactSubviews]) { UIViewController *controller = tab.reactViewController; if (!controller) { - controller = [[RCTWrapperViewController alloc] initWithContentView:tab - eventDispatcher:_eventDispatcher]; + controller = [[RCTWrapperViewController alloc] initWithContentView:tab]; } [viewControllers addObject:controller]; } @@ -154,7 +148,7 @@ - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectView { NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; RCTTabBarItem *tab = [self reactSubviews][index]; - [_eventDispatcher sendInputEventWithName:@"press" body:@{@"target": tab.reactTag}]; + if (tab.onPress) tab.onPress(nil); return NO; } diff --git a/React/Views/RCTTabBarItem.h b/React/Views/RCTTabBarItem.h index 8fe6d8efbd64a1..def1abf6c34748 100644 --- a/React/Views/RCTTabBarItem.h +++ b/React/Views/RCTTabBarItem.h @@ -9,10 +9,13 @@ #import +#import "RCTComponent.h" + @interface RCTTabBarItem : UIView @property (nonatomic, copy) id icon; @property (nonatomic, assign, getter=isSelected) BOOL selected; @property (nonatomic, readonly) UITabBarItem *barItem; +@property (nonatomic, copy) RCTBubblingEventBlock onPress; @end diff --git a/React/Views/RCTTabBarItemManager.m b/React/Views/RCTTabBarItemManager.m index f91153efd19d6d..a926a54f343fc3 100644 --- a/React/Views/RCTTabBarItemManager.m +++ b/React/Views/RCTTabBarItemManager.m @@ -21,10 +21,11 @@ - (UIView *)view return [RCTTabBarItem new]; } -RCT_EXPORT_VIEW_PROPERTY(selected, BOOL); -RCT_EXPORT_VIEW_PROPERTY(icon, id); -RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage, UIImage); -RCT_REMAP_VIEW_PROPERTY(badge, barItem.badgeValue, NSString); +RCT_EXPORT_VIEW_PROPERTY(selected, BOOL) +RCT_EXPORT_VIEW_PROPERTY(icon, id) +RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage, UIImage) +RCT_REMAP_VIEW_PROPERTY(badge, barItem.badgeValue, NSString) +RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem) { view.barItem.title = json ? [RCTConvert NSString:json] : defaultView.barItem.title; diff --git a/React/Views/RCTTabBarManager.m b/React/Views/RCTTabBarManager.m index 7b96162465b267..b5d79f88602182 100644 --- a/React/Views/RCTTabBarManager.m +++ b/React/Views/RCTTabBarManager.m @@ -18,7 +18,7 @@ @implementation RCTTabBarManager - (UIView *)view { - return [[RCTTabBar alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; + return [RCTTabBar new]; } RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 6e7019f58c597d..1222dc9cf7d02d 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -11,17 +11,20 @@ #import +#import "RCTComponent.h" #import "RCTPointerEvents.h" @protocol RCTAutoInsetsProtocol; @class RCTView; -typedef void (^RCTViewEventHandler)(RCTView *view); @interface RCTView : UIView -@property (nonatomic, copy) RCTViewEventHandler accessibilityTapHandler; -@property (nonatomic, copy) RCTViewEventHandler magicTapHandler; +/** + * Accessibility event handlers + */ +@property (nonatomic, copy) RCTDirectEventBlock onAccessibilityTap; +@property (nonatomic, copy) RCTDirectEventBlock onMagicTap; /** * Used to control how touch events are processed. diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index d242faa44a6996..85ff2ecf4f5f3f 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -167,8 +167,8 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event - (BOOL)accessibilityActivate { - if (self.accessibilityTapHandler) { - self.accessibilityTapHandler(self); + if (_onAccessibilityTap) { + _onAccessibilityTap(nil); return YES; } else { return NO; @@ -177,8 +177,8 @@ - (BOOL)accessibilityActivate - (BOOL)accessibilityPerformMagicTap { - if (self.magicTapHandler) { - self.magicTapHandler(self); + if (_onMagicTap) { + _onMagicTap(nil); return YES; } else { return NO; diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index f3c370b9472cdd..35c2a4d4fc8531 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -11,10 +11,12 @@ #import "RCTBridgeModule.h" #import "RCTConvert.h" +#import "RCTComponent.h" +#import "RCTDefines.h" +#import "RCTEventDispatcher.h" #import "RCTLog.h" @class RCTBridge; -@class RCTEventDispatcher; @class RCTShadowView; @class RCTSparseArray; @class RCTUIManager; @@ -37,7 +39,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * 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; +- (UIView *)view; /** * This method instantiates a native view using the props passed into the component. @@ -57,6 +59,8 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v - (RCTShadowView *)shadowView; /** + * DEPRECATED: declare properties of type RCTBubblingEventBlock instead + * * Returns an array of names of events that can be sent by native views. This * should return bubbling, directly-dispatched event types. The event name * should not include a prefix such as 'on' or 'top', as this will be applied @@ -69,6 +73,8 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v - (NSArray *)customBubblingEventTypes; /** + * DEPRECATED: declare properties of type RCTDirectEventBlock instead + * * Returns an array of names of events that can be sent by native views. This * should return non-bubbling, directly-dispatched event types. The event name * should not include a prefix such as 'on' or 'top', as this will be applied diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index b4562f689d6d95..c2cf93676b4276 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -54,7 +54,7 @@ - (dispatch_queue_t)methodQueue return _bridge.uiManager.methodQueue; } -- (UIView *)viewWithProps:(NSDictionary *)props +- (UIView *)viewWithProps:(__unused NSDictionary *)props { return [self view]; } @@ -76,7 +76,6 @@ - (NSArray *)customBubblingEventTypes // Generic events @"press", @"change", - @"change", @"focus", @"blur", @"submitEditing", @@ -92,11 +91,7 @@ - (NSArray *)customBubblingEventTypes - (NSArray *)customDirectEventTypes { - return @[ - @"layout", - @"accessibilityTap", - @"magicTap", - ]; + return @[]; } - (NSDictionary *)constantsToExport @@ -195,27 +190,8 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused RCTSpars view.layer.borderWidth = json ? [RCTConvert CGFloat:json] : defaultView.layer.borderWidth; } } -RCT_CUSTOM_VIEW_PROPERTY(onAccessibilityTap, BOOL, __unused RCTView) -{ - view.accessibilityTapHandler = [self eventHandlerWithName:@"accessibilityTap" json:json]; -} -RCT_CUSTOM_VIEW_PROPERTY(onMagicTap, BOOL, __unused RCTView) -{ - view.magicTapHandler = [self eventHandlerWithName:@"magicTap" json:json]; -} - -- (RCTViewEventHandler)eventHandlerWithName:(NSString *)eventName json:(id)json -{ - RCTViewEventHandler handler = nil; - if ([RCTConvert BOOL:json]) { - __weak RCTViewManager *weakSelf = self; - handler = ^(RCTView *tappedView) { - NSDictionary *body = @{ @"target": tappedView.reactTag }; - [weakSelf.bridge.eventDispatcher sendInputEventWithName:eventName body:body]; - }; - } - return handler; -} +RCT_EXPORT_VIEW_PROPERTY(onAccessibilityTap, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onMagicTap, RCTDirectEventBlock) #define RCT_VIEW_BORDER_PROPERTY(SIDE) \ RCT_CUSTOM_VIEW_PROPERTY(border##SIDE##Width, CGFloat, RCTView) \ @@ -291,6 +267,6 @@ - (RCTViewEventHandler)eventHandlerWithName:(NSString *)eventName json:(id)json RCT_EXPORT_SHADOW_PROPERTY(alignSelf, css_align_t) RCT_EXPORT_SHADOW_PROPERTY(position, css_position_type_t) -RCT_EXPORT_SHADOW_PROPERTY(onLayout, BOOL) +RCT_EXPORT_SHADOW_PROPERTY(onLayout, RCTDirectEventBlock) @end diff --git a/React/Views/RCTWebView.h b/React/Views/RCTWebView.h index 9a028ea49eade9..fdb192a39f3f31 100644 --- a/React/Views/RCTWebView.h +++ b/React/Views/RCTWebView.h @@ -17,8 +17,6 @@ */ extern NSString *const RCTJSNavigationScheme; -@class RCTEventDispatcher; - @interface RCTWebView : RCTView @property (nonatomic, strong) NSURL *URL; @@ -26,8 +24,6 @@ extern NSString *const RCTJSNavigationScheme; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; @property (nonatomic, copy) NSString *injectedJavaScript; -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - - (void)goForward; - (void)goBack; - (void)reload; diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 196284d24bf4b4..4838179a520af1 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -22,24 +22,24 @@ @interface RCTWebView () +@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; +@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; +@property (nonatomic, copy) RCTDirectEventBlock onLoadingError; + @end @implementation RCTWebView { - RCTEventDispatcher *_eventDispatcher; UIWebView *_webView; NSString *_injectedJavaScript; } -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithFrame:(CGRect)frame { - RCTAssertParam(eventDispatcher); - - if ((self = [super initWithFrame:CGRectZero])) { + if ((self = [super initWithFrame:frame])) { super.backgroundColor = [UIColor clearColor]; _automaticallyAdjustContentInsets = YES; _contentInset = UIEdgeInsetsZero; - _eventDispatcher = eventDispatcher; _webView = [[UIWebView alloc] initWithFrame:self.bounds]; _webView.delegate = self; [self addSubview:_webView]; @@ -47,7 +47,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher return self; } -RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)goForward @@ -123,13 +122,10 @@ - (UIColor *)backgroundColor - (NSMutableDictionary *)baseEvent { - NSURL *url = _webView.request.URL; - NSString *title = [_webView stringByEvaluatingJavaScriptFromString:@"document.title"]; NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{ - @"target": self.reactTag, - @"url": url ? url.absoluteString : @"", + @"url": _webView.request.URL.absoluteString ?: @"", @"loading" : @(_webView.loading), - @"title": title, + @"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"], @"canGoBack": @(_webView.canGoBack), @"canGoForward" : @(_webView.canGoForward), }]; @@ -139,19 +135,20 @@ - (NSMutableDictionary *)baseEvent #pragma mark - UIWebViewDelegate methods - - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { - // We have this check to filter out iframe requests and whatnot - BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; - if (isTopFrame) { - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{ - @"url": (request.URL).absoluteString, - @"navigationType": @(navigationType) - }]; - [_eventDispatcher sendInputEventWithName:@"loadingStart" body:event]; + if (_onLoadingStart) { + // We have this check to filter out iframe requests and whatnot + BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; + if (isTopFrame) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": (request.URL).absoluteString, + @"navigationType": @(navigationType) + }]; + _onLoadingStart(event); + } } // JS Navigation handler @@ -160,21 +157,24 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error { - if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { - // NSURLErrorCancelled is reported when a page has a redirect OR if you load - // a new URL in the WebView before the previous one came back. We can just - // ignore these since they aren't real errors. - // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os - return; - } + if (_onLoadingError) { - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{ - @"domain": error.domain, - @"code": @(error.code), - @"description": error.localizedDescription, - }]; - [_eventDispatcher sendInputEventWithName:@"loadingError" body:event]; + if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { + // NSURLErrorCancelled is reported when a page has a redirect OR if you load + // a new URL in the WebView before the previous one came back. We can just + // ignore these since they aren't real errors. + // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os + return; + } + + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"domain": error.domain, + @"code": @(error.code), + @"description": error.localizedDescription, + }]; + _onLoadingError(event); + } } - (void)webViewDidFinishLoad:(UIWebView *)webView @@ -184,8 +184,8 @@ - (void)webViewDidFinishLoad:(UIWebView *)webView } // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. - if (!webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) { - [_eventDispatcher sendInputEventWithName:@"loadingFinish" body:[self baseEvent]]; + if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) { + _onLoadingFinish([self baseEvent]); } } diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index 1fd2cfe3f9b214..8779a970baa81d 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -20,7 +20,7 @@ @implementation RCTWebViewManager - (UIView *)view { - return [[RCTWebView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; + return [RCTWebView new]; } RCT_REMAP_VIEW_PROPERTY(url, URL, NSURL); @@ -31,15 +31,9 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString); RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets); RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL); - -- (NSArray *)customDirectEventTypes -{ - return @[ - @"loadingStart", - @"loadingFinish", - @"loadingError", - ]; -} +RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock); - (NSDictionary *)constantsToExport { @@ -80,7 +74,6 @@ - (NSDictionary *)constantsToExport }]; } - RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { diff --git a/React/Views/RCTWrapperViewController.h b/React/Views/RCTWrapperViewController.h index 09e64789170359..7b24ae7d53072f 100644 --- a/React/Views/RCTWrapperViewController.h +++ b/React/Views/RCTWrapperViewController.h @@ -11,7 +11,6 @@ #import "RCTViewControllerProtocol.h" -@class RCTEventDispatcher; @class RCTNavItem; @class RCTWrapperViewController; @@ -24,11 +23,8 @@ didMoveToNavigationController:(UINavigationController *)navigationController; @interface RCTWrapperViewController : UIViewController -- (instancetype)initWithContentView:(UIView *)contentView - eventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - -- (instancetype)initWithNavItem:(RCTNavItem *)navItem - eventDispatcher:(RCTEventDispatcher *)eventDispatcher; +- (instancetype)initWithContentView:(UIView *)contentView NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithNavItem:(RCTNavItem *)navItem; @property (nonatomic, weak) id navigationListener; @property (nonatomic, strong) RCTNavItem *navItem; diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index 00af3b53dada3c..92707e683f02ba 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -21,7 +21,6 @@ @implementation RCTWrapperViewController { UIView *_wrapperView; UIView *_contentView; - RCTEventDispatcher *_eventDispatcher; CGFloat _previousTopLayout; CGFloat _previousBottomLayout; } @@ -30,23 +29,19 @@ @implementation RCTWrapperViewController @synthesize currentBottomLayoutGuide = _currentBottomLayoutGuide; - (instancetype)initWithContentView:(UIView *)contentView - eventDispatcher:(RCTEventDispatcher *)eventDispatcher { RCTAssertParam(contentView); - RCTAssertParam(eventDispatcher); if ((self = [super initWithNibName:nil bundle:nil])) { _contentView = contentView; - _eventDispatcher = eventDispatcher; self.automaticallyAdjustsScrollViewInsets = NO; } return self; } - (instancetype)initWithNavItem:(RCTNavItem *)navItem - eventDispatcher:(RCTEventDispatcher *)eventDispatcher { - if ((self = [self initWithContentView:navItem eventDispatcher:eventDispatcher])) { + if ((self = [self initWithContentView:navItem])) { _navItem = navItem; } return self; @@ -101,14 +96,8 @@ - (void)viewWillAppear:(BOOL)animated UINavigationItem *item = self.navigationItem; item.title = _navItem.title; item.backBarButtonItem = _navItem.backButtonItem; - if ((item.leftBarButtonItem = _navItem.leftButtonItem)) { - item.leftBarButtonItem.target = self; - item.leftBarButtonItem.action = @selector(handleNavLeftButtonTapped); - } - if ((item.rightBarButtonItem = _navItem.rightButtonItem)) { - item.rightBarButtonItem.target = self; - item.rightBarButtonItem.action = @selector(handleNavRightButtonTapped); - } + item.leftBarButtonItem = _navItem.leftButtonItem; + item.rightBarButtonItem = _navItem.rightButtonItem; } } @@ -122,18 +111,6 @@ - (void)loadView self.view = _wrapperView; } -- (void)handleNavLeftButtonTapped -{ - [_eventDispatcher sendInputEventWithName:@"navLeftButtonTap" - body:@{@"target":_navItem.reactTag}]; -} - -- (void)handleNavRightButtonTapped -{ - [_eventDispatcher sendInputEventWithName:@"navRightButtonTap" - body:@{@"target":_navItem.reactTag}]; -} - - (void)didMoveToParentViewController:(UIViewController *)parent { // There's no clear setter for navigation controllers, but did move to parent diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index 18bd5a747cc8d2..1cbbc16da548b2 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -23,7 +23,7 @@ - (NSNumber *)reactTag - (void)setReactTag:(NSNumber *)reactTag { - objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_COPY_NONATOMIC); + objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isReactRootView