Skip to content

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

Merged
merged 12 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 141 additions & 25 deletions ios/FullStory.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

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:])?

Copy link
Contributor

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.

Copy link
Contributor

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 order object_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.)

NSArray <NSString *>* classes = [(NSString *)json componentsSeparatedByString: @","];
[FS removeAllClasses:view];
[FS addClasses:view classNames:classes];
}

static void set_fsTagName(id json, RCTView *view) {
[FS setTagName:view tagName:(NSString *)json];
}

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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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);

Choose a reason for hiding this comment

The 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
#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
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);

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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);
}
}
}
}
});
#endif
}
@end
Loading