-
Notifications
You must be signed in to change notification settings - Fork 12
iOS Fabric implementation #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
869454a
d2047f4
9312d02
dffcceb
f01f58b
e541035
cc0594d
6e80826
8d9634a
9179597
49a389f
b3edbe2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -216,16 +216,61 @@ - (void)log:(double)logLevel message:(NSString *)message { | |
|
||
static const char *_rctview_previous_attributes_key = "associated_object_rctview_previous_attributes_key"; | ||
|
||
static void set_fsClass(id json, RCTView *view) { | ||
NSArray <NSString *>* classes = [(NSString *)json componentsSeparatedByString: @","]; | ||
jwise marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[FS removeAllClasses:view]; | ||
[FS addClasses:view classNames:classes]; | ||
} | ||
|
||
static void set_fsTagName(id json, RCTView *view) { | ||
[FS setTagName:view tagName:(NSString *)json]; | ||
} | ||
jwise marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
static void set_dataComponent(id json, RCTView *view) { | ||
[FS setAttribute:view attributeName:@"data-component" attributeValue:(NSString *)json]; | ||
} | ||
|
||
static void set_dataElement(id json, RCTView *view) { | ||
[FS setAttribute:view attributeName:@"data-element" attributeValue:(NSString *)json]; | ||
} | ||
|
||
static void set_dataSourceFile(id json, RCTView *view) { | ||
[FS setAttribute:view attributeName:@"data-source-file" attributeValue:(NSString *)json]; | ||
} | ||
|
||
static void set_fsAttribute(id json, RCTView *view) { | ||
NSDictionary *newAttrs = (NSDictionary *)json; | ||
|
||
/* Clear up all the old attributes first, if they exist. */ | ||
NSSet *oldAttrs = objc_getAssociatedObject(view, _rctview_previous_attributes_key); | ||
if (oldAttrs) { | ||
for (NSString *attr in oldAttrs) { | ||
[FS removeAttribute:view attributeName:attr]; | ||
} | ||
} | ||
|
||
/* Load in the new attributes. */ | ||
NSMutableSet *newAttrSet = [NSMutableSet new]; | ||
if (newAttrs) { | ||
for (NSString *attr in newAttrs) { | ||
[FS setAttribute:view attributeName:attr attributeValue:(NSString *)newAttrs[attr]]; | ||
[newAttrSet addObject:attr]; | ||
} | ||
} | ||
|
||
/* And set them up for cleanup next time. */ | ||
objc_setAssociatedObject(view, _rctview_previous_attributes_key, newAttrSet, OBJC_ASSOCIATION_RETAIN); | ||
} | ||
|
||
|
||
@implementation RCTViewManager (FullStory) | ||
|
||
+ (NSArray<NSString *> *) propConfig_fsClass { | ||
return @[@"NSString *", @"__custom__"]; | ||
} | ||
|
||
- (void) set_fsClass:(id)json forView:(RCTView*)view withDefaultView:(RCTView *)defaultView { | ||
NSArray <NSString *>* classes = [(NSString *)json componentsSeparatedByString: @","]; | ||
[FS removeAllClasses:view]; | ||
[FS addClasses:view classNames:classes]; | ||
set_fsClass(json, view); | ||
} | ||
|
||
+ (NSArray<NSString *> *) propConfig_dataComponent { | ||
|
@@ -257,40 +302,46 @@ - (void) set_dataSourceFile:(id)json forView:(RCTView*)view withDefaultView:(RCT | |
} | ||
|
||
- (void) set_fsTagName:(id)json forView:(RCTView*)view withDefaultView:(RCTView *)defaultView { | ||
[FS setTagName:view tagName:(NSString *)json]; | ||
set_fsTagName(json, view); | ||
} | ||
|
||
+ (NSArray<NSString *> *) propConfig_fsAttribute { | ||
return @[@"NSDictionary *", @"__custom__"]; | ||
} | ||
|
||
- (void) set_fsAttribute:(id)json forView:(RCTView*)view withDefaultView:(RCTView *)defaultView { | ||
NSDictionary *newAttrs = (NSDictionary *)json; | ||
|
||
/* Clear up all the old attributes first, if they exist. */ | ||
NSSet *oldAttrs = objc_getAssociatedObject(view, _rctview_previous_attributes_key); | ||
if (oldAttrs) { | ||
for (NSString *attr in oldAttrs) { | ||
[FS removeAttribute:view attributeName:attr]; | ||
} | ||
} | ||
|
||
/* Load in the new attributes. */ | ||
NSMutableSet *newAttrSet = [NSMutableSet new]; | ||
if (newAttrs) { | ||
for (NSString *attr in newAttrs) { | ||
[FS setAttribute:view attributeName:attr attributeValue:(NSString *)newAttrs[attr]]; | ||
[newAttrSet addObject:attr]; | ||
} | ||
} | ||
|
||
/* And set them up for cleanup next time. */ | ||
objc_setAssociatedObject(view, _rctview_previous_attributes_key, newAttrSet, OBJC_ASSOCIATION_RETAIN); | ||
set_fsAttribute(json, view); | ||
} | ||
@end | ||
|
||
@interface FSReactSwizzleBootstrap : NSObject | ||
@end | ||
#define SWIZZLE_HANDLE_COMMAND(rct_clazz) \ | ||
SWIZZLE_BEGIN_INSTANCE(rct_clazz, @selector(handleCommand:args:), void, const NSString *commandName, const NSArray *args) { \ | ||
if ([commandName isEqualToString:@"fsAttribute"]) { \ | ||
set_fsAttribute(args[0], self); \ | ||
} else if ([commandName isEqualToString:@"fsClass"]) { \ | ||
set_fsClass(args[0], self); \ | ||
} else if ([commandName isEqualToString:@"dataElement"]) { \ | ||
set_dataElement(args[0], self); \ | ||
} else if ([commandName isEqualToString:@"dataSourceFile"]) { \ | ||
set_dataSourceFile(args[0], self); \ | ||
} else if ([commandName isEqualToString:@"fsTagName"]) { \ | ||
set_fsTagName(args[0], self); \ | ||
} else if ([commandName isEqualToString:@"dataComponent"]) { \ | ||
set_dataComponent(args[0], self); \ | ||
} else { \ | ||
SWIZZLED_METHOD(commandName, args); \ | ||
} \ | ||
} SWIZZLE_END | ||
|
||
static bool array_contains_string(const char **array, const char *string) { | ||
if (string == nullptr || array == nullptr) return false; | ||
for (; *array != nullptr; array++) { | ||
if (strcmp(*array, string) == 0) return true; | ||
} | ||
return false; | ||
} | ||
|
||
@implementation FSReactSwizzleBootstrap | ||
+ (void) load { | ||
|
@@ -309,5 +360,70 @@ + (void) load { | |
r[@"propTypes"][@"fsAttribute"] = @"NSDictionary *"; | ||
return r; | ||
} SWIZZLE_END; | ||
|
||
// To store each original method in its own separate location, | ||
// we separately swizzle each class manually | ||
#pragma clang diagnostic push | ||
#pragma clang diagnostic ignored "-Wundeclared-selector" | ||
SWIZZLE_HANDLE_COMMAND(RCTViewComponentView); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i like this, super easy to understand what the swizzles are. |
||
SWIZZLE_HANDLE_COMMAND(RCTTextInputComponentView); | ||
SWIZZLE_HANDLE_COMMAND(RCTSwitchComponentView); | ||
SWIZZLE_HANDLE_COMMAND(RCTScrollViewComponentView); | ||
SWIZZLE_HANDLE_COMMAND(RCTPullToRefreshViewComponentView); | ||
SWIZZLE_HANDLE_COMMAND(RCTLegacyViewManagerInteropComponentView); | ||
#pragma clang pop | ||
#ifdef DEBUG | ||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ | ||
#pragma clang diagnostic push | ||
RyanCommits marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#pragma clang diagnostic ignored "-Wundeclared-selector" | ||
SEL sel = @selector(handleCommand:args:); | ||
#pragma clang pop | ||
const char* swizzled_classes[] = { | ||
"RCTViewComponentView", | ||
"RCTTextInputComponentView", | ||
"RCTSwitchComponentView", | ||
"RCTScrollViewComponentView", | ||
"RCTPullToRefreshViewComponentView", | ||
"RCTLegacyViewManagerInteropComponentView", | ||
0}; | ||
// Grab the impl of RCTViewComponentView | ||
Class viewComponentView = NSClassFromString(@"RCTViewComponentView"); | ||
Method _Nullable swizzledViewComponentViewCommand = class_getInstanceMethod(viewComponentView, sel); | ||
IMP swizzledViewComponentViewCommandImplementation = method_getImplementation(swizzledViewComponentViewCommand); | ||
|
||
int classCount = objc_getClassList(NULL, 0); | ||
// The classes stored in this array may not be NSObject classes, | ||
// so may not respond to retain messages. See: | ||
// https://gist.github.com/mikeash/1267596 | ||
__unsafe_unretained Class classes[classCount]; | ||
// Install swizzle in all subclasses of RCTViewComponentView | ||
RyanCommits marked this conversation as resolved.
Show resolved
Hide resolved
|
||
objc_getClassList(classes, classCount); | ||
for (int i = 0; i < classCount; i++) { | ||
Class _Nullable __unsafe_unretained cls = classes[i]; | ||
while (cls && cls != viewComponentView) { | ||
cls = class_getSuperclass(cls); | ||
} | ||
if (cls == viewComponentView && cls && !array_contains_string(swizzled_classes, class_getName(classes[i]))) { | ||
cls = classes[i]; | ||
const char *className = class_getName(cls); | ||
// Note that this will find the superclass's implementation, | ||
// which informs us whether this class gets its implementation | ||
// from the swizzle above. | ||
// Since we skip the explicitly swizzled classes, each with | ||
// its own implementation, the remaining classes should either | ||
// have the superclass implementation, or their own, which is | ||
// incapable of receiving FS properties. | ||
Method _Nullable existingMethod = class_getInstanceMethod(cls, sel); | ||
if (existingMethod) { | ||
IMP existingImplementation = method_getImplementation(existingMethod); | ||
if (existingImplementation != swizzledViewComponentViewCommandImplementation) { | ||
NSAssert(strncmp(className, "RCT", 3) != 0, @"React Native framework class %s needs handleCommand support! Please contact FullStory support with this message.", className); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not the most familiar with NSAsserts really - does this quit the app like a crash? I guess like, if a developer ends up in this block, would they be able to still use their app in debug mode? it would kinda suck if a customer has to update their RN fullstory version in the middle of a development session because we're NSAsserting something here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it would crash the app when it is running in debug mode. I agree that this would be very disruptive to the developer. However, it's kind of unsafe for them to use the SDK if this is the case. If we are worried that this assert will be triggered, we could instead swizzle all subclasses per the original design. Or, we could add a terminate-with-error-message method to the SDK in a followup PR and terminate with this error message instead of NSAssert. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm ok with this being a crashing NSAssert -- especially because a developer wanting to work around it doesn't need to wait for us to release a new plugin version, but instead can make the modification themselves in their local tree. Nice work on checking this in debug. |
||
NSLog(@"RCTViewComponentView subclass %s cannot receive FullStory commands", className); | ||
RyanCommits marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
} | ||
}); | ||
#endif | ||
} | ||
@end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be more idiomatic to have this as an extension on
RCTView
(i.e.,-[RCTView _fs_set_fsClass:]
)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a static function is the correct idiom to use when the functionality's use is limited to the current file. Extensions are meant to allow use across files - they add functions to the lookup table, and carry the risk of being referenced outside of their intended use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, fair enough. (Generally I tend to prefix static methods with
_
, and I tend to prefer the parameter orderobject_verb_noun(object, parameters...)
, which in this case would translate to_rctview_set_fsClass(RCTView *view, id json)
, just to scope things a little more tightly -- like we do two lines prior, scoping_rctview_previous_attributes_key
by name -- but that could be a matter of taste.)