• iOS全埋点解决方案UITableView和UICollectionView点击事件


    前言

    在 $AppClick 事件采集中,还有两个比较特殊的控件:

    • UITableView
    • •UICollectionView

    这两个控件的点击事件,一般指的是点击 UITableViewCell 和 UICollectionViewCell。而 UITableViewCell 和 UICollectionViewCell 都是直接继承自 UIView 类,而不是 UIControl 类。因此,我们之前实现 $AppClick 事件全埋点的两个方案均不适用于 UITableView 和 UICollectionView。

    关于实现 UITableView 和 UICollectionView $AppClick 事件的全埋点,常见的方案有三种:

    • 方法交换
    • 动态子类
    • 消息转发

    这三种方案,各有优缺点。

    下面,我们以 UITableView 控件为例,来分别介绍如何使用这三种方案实现 $AppClick 事件的全埋点。

    一、支持 UITableView 控件

    1.1 方案一:方法交换

    ​ 大概思路:首先,我们使用 Method Swizzling 交换 UITableView 的 - setDelegate: 方法,然后能获取到实现了 UITableViewDelegate 协议的 delegate 对象,在拿到 delegate 对象之后,就可以交换 delegate 对象的 - tableView:didSelectRowAtIndexPath: 方法,最后,在交换后的方法中触发 $AppClick 事件,从而达到全埋点的效果。

    实现步骤:

    第一步: 添加 UITableView+SensorsData 类别,在类别中实现 + load 类方法,并在 + load 类方法中交换 - setDelegate: 方法

    + (void)load {
        [UITableView sensorsdata_swizzleMethod:@selector(setDelegate:) withMethod:@selector(sensorsdata_setDelegate:)];
    }
    
    - (void)sensorsdata_setDelegate:(id<UITableViewDelegate>)delegate {
        
        // 调用原始的设置代理方法
        [self sensorsdata_setDelegate:delegate];
    }
    

    第二步:添加 sensorsdata_tableViewDidSelectRow 函数

    #import <objc/message.h>
    
    static void sensorsdata_tableViewDidSelectRow(id object, SEL selector, UITableView *tableView, NSIndexPath *indexPath) {
        SEL destinationSelecotr = NSSelectorFromString(@"sensorsdata_tableView:didSelectRowAtIndexPath:");
        // 通过消息发送,调用原始的 tableView:didSelectRowAtIndexPath: 方法实现
        ((void(*)(id, SEL, id, id))objc_msgSend)(object, destinationSelecotr, tableView, indexPath);
    
        // 触发 $AppClick 事件
    }
    

    第三步:添加一个私有方法 - sensorsdata_swizzleDidSelectRowIndexPathMethodWithDelegate: 负责给 delegate 添加一个方法并进行替换

    #import "NSObject+SASwizzler.h"
    
    - (void)sensorsdata_swizzleDidSelectRowIndexPathMethodWithDelegate:(id)delegate {
        // 获取 delegate 对象的类
        Class delegateClass = [delegate class];
        // 方法名
        SEL sourceSelector = @selector(tableView:didSelectRowAtIndexPath:);
        // 当 delegate 对象中没有实现 tableView:didSelectRowAtIndexPath: 方法时,直接返回
        if (![delegate respondsToSelector:sourceSelector]) {
            return;
        }
        
        SEL destinationSelecrot = @selector(sensorsdata_tableView:didSelectRowAtIndexPath:);
        //当 delegate 对象中已经存在实现 sensorsdata_tableView:didSelectRowAtIndexPath: 方法时,说明已经交换,直接返回
        if ([delegate respondsToSelector:destinationSelecrot]) {
            return;
        }
        
        Method souceMethod = class_getInstanceMethod(delegateClass, sourceSelector);
        const char *encoding = method_getTypeEncoding(souceMethod);
        if (!class_addMethod([delegate class], destinationSelecrot, sensorsdata_tableViewDidSelectRow, encoding)) {
            NSLog(@"Add %@ to %@ error", NSStringFromSelector(sourceSelector), [delegate class]);
            return;
        }
        
        // 方法添加成功后进行方法交换
        [delegateClass sensorsdata_swizzleMethod:sourceSelector withMethod:destinationSelecrot];
    }
    

    第四步:在 - sensorsdata_setDelegate:方法中调用 - sensorsdata_swizzleDidSelectRowIndexPathMethodWithDelegate:方法进行交换

    - (void)sensorsdata_setDelegate:(id<UITableViewDelegate>)delegate {
        
        // 调用原始的设置代理方法
        [self sensorsdata_setDelegate:delegate];
        
        // 方案一: 方法交换
        // 交换 delegate 对象中的 tableView:didSelectRowAtIndexPath: 方法
        [self sensorsdata_swizzleDidSelectRowIndexPathMethodWithDelegate:delegate];
    }
    

    第五步:在 SensorsAnalyticsSDK+Track 中新增一个触发 UITableView 控件点击事件的方法 - trackAppClickWithTableView: didSelectRowAtIndexPath: properties: 。

    @interface SensorsAnalyticsSDK (Track)
    
    /// 支持 UITableView 触发 $AppClick 事件
    /// @param tableView 触发事件的 tableView 视图
    /// @param indexPath 在 tableView 中点击的位置
    /// @param properties 自定义事件参数
    - (void)trackAppClickWithTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath properties:(nullable NSDictionary <NSString*, id> *)properties;
    
    @end
    
    - (void)trackAppClickWithTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath properties:(nullable NSDictionary <NSString*, id> *)properties {
        
        NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
        // TODO: 获取用户点击的 UITableViewCell 控件对象
        // TODO: 设置被用户点击的 UITableViewCell 控件上的内容
        // TODO: 设置被用户点击 UITableViewCell 控件所在的位置
        
        // 添加自定义属性
        [eventProperties addEntriesFromDictionary:properties];
        [[SensorsAnalyticsSDK sharedInstance] trackAppClickWithView:tableView properties:properties];
    }
    

    第六步:在 sensorsdata_tableViewDidSelectRow 函数中触发 $AppClick 事件

    static void sensorsdata_tableViewDidSelectRow(id object, SEL selector, UITableView *tableView, NSIndexPath *indexPath) {
        SEL destinationSelecotr = NSSelectorFromString(@"sensorsdata_tableView:didSelectRowAtIndexPath:");
        // 通过消息发送,调用原始的 tableView:didSelectRowAtIndexPath: 方法实现
        ((void(*)(id, SEL, id, id))objc_msgSend)(object, destinationSelecotr, tableView, indexPath);
    
        // 触发 $AppClick 事件
        [[SensorsAnalyticsSDK sharedInstance] trackAppClickWithTableView:tableView didSelectRowAtIndexPath:indexPath properties:nil];
    }
    

    第七步:测试运行

    ​ 在 Demo 中添加 tableView, 点击 tableView 上的 cell

    {
      "event" : "$AppClick",
      "time" : 1648801408348,
      "propeerties" : {
        "$model" : "x86_64",
        "$manufacturer" : "Apple",
        "$element_type" : "UITableView",
        "$lib_version" : "1.0.0",
        "$os" : "iOS",
        "$app_version" : "1.0",
        "$screen_name" : "ViewController",
        "$os_version" : "15.2",
        "$lib" : "iOS"
      }
    }
    

    ​ 至此,已经通过方法交换实现了 UITableView 的 $AppClick 事件。

    1.2 方案二:动态子类

    ​ 大概思路:动态子类的方案,就是在运行时,给实现了 UITableViewDelegate 协议的 - tableView:didSelectRowAtIndexPath: 方法的类创建一个子类,让这个类的对象变成我们自己创建的子类的对象。同时,还需要在创建的子类中动态添加 - tableView:didSelectRowAtIndexPath: 方法。那么,当用户点击 UITableViewCell 时,就会先运行我们创建的子类中的 - tableView:didSelectRowAtIndexPath: 方法。然后,我们在实现这个方法的时候,先调用 delegate 原来的方法实现再触发 $AppClick 事件,即可达到全埋点的效果。

    实现步骤:

    第一步:在项目创建一个动态添加子类的工具类 SensrosAnalyticsDynamicDelegate。在工具类 SensrosAnalyticsDynamicDelegate 中添加 - tableView: didSelectRowAtIndexPath: 方法。

    #import "SensrosAnalyticsDynamicDelegate.h"
    
    #import "SensorsAnalyticsSDK+Track.h"
    #import <objc/runtime.h>
    
    /// delegate 对象的之类前缀
    static NSString *const kSensorsDelegatePrefix = @"cn.SensorsData";
    
    /// tableView:didSelectRowAtIndexPath: 方法指针类型
    typedef void (*SensorsDidSelectImplementation)(id, SEL, UITableView *, NSIndexPath *);
    
    @implementation SensrosAnalyticsDynamicDelegate
    
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
        // 第一步: 获取原始类
        Class cla = object_getClass(tableView);
        NSString *className = [NSStringFromClass(cla) stringByReplacingOccurrencesOfString:kSensorsDelegatePrefix withString:@""];
        Class originalClass = objc_getClass([className UTF8String]);
        
        // 第二步:调用开发者自己实现的方法
        SEL originalSelector = NSSelectorFromString(@"tableView:didSelectRowAtIndexPath:");
        Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
        IMP originalImplementation = method_getImplementation(originalMethod);
        if (originalImplementation) {
            ((SensorsDidSelectImplementation)originalImplementation)(tableView.delegate, originalSelector, tableView, indexPath);
        }
        
        // 第三步:埋点
        [[SensorsAnalyticsSDK sharedInstance] trackAppClickWithTableView:tableView didSelectRowAtIndexPath:indexPath properties:nil];
    }
    @end
    

    第二步:在 SensrosAnalyticsDynamicDelegate 类中添加 - proxyWithTableViewDelegate:类方法

    + (void)proxyWithTableViewDelegate:(id<UITableViewDelegate>)delegate {
        SEL originalSelector = NSSelectorFromString(@"tableView:didSelectRowAtIndexPath:");
        // 当 delegate 对象中没有实现 tableView:didSelectRowAtIndexPath: 方法时,直接返回
        if (![delegate respondsToSelector:originalSelector]) {
            return;
        }
        
        // 动态创建一个新类
        Class originalClass = object_getClass(delegate);
        NSString *originalClassName = NSStringFromClass(originalClass);
        // 当 delegate 对象已经是一个动态创建的类时,无需重复创建,,直接返回
        if ([originalClassName hasPrefix:kSensorsDelegatePrefix]) {
            return;
        }
        
        NSString *subClassName = [kSensorsDelegatePrefix stringByAppendingString:originalClassName];
        Class subClass = NSClassFromString(subClassName);
        if (!subClass) {
            // 注册一个新的子类,其父类为originalClass
            subClass = objc_allocateClassPair(originalClass, subClassName.UTF8String, 0);
            
            // 获取 SensrosAnalyticsDynamicDelegate 中的 tableView:didSelecorRowIndexPath: 方法指针
            Method method = class_getInstanceMethod(self, originalSelector);
            // 获取方法实现
            IMP methodIMP = method_getImplementation(method);
            // 获取方法类型编码
            const char *types = method_getTypeEncoding(method);
            // 在 subClass 中添加 tableView:didSelectRowAtIndexPath: 方法
            if (!class_addMethod(subClass, originalSelector, methodIMP, types)) {
                NSLog(@"Cannot copy method to destination selector %@ as it already exists", NSStringFromSelector(originalSelector));
            }
            
            // 子类和原始类的大小必须相同 ,不能有更多的成员变量或者属性
            // 如果不同,将导致设置新的子类时,重新分配内存,重写对象的 isa 指针
            if (class_getInstanceSize(originalClass) != class_getInstanceSize(subClass)) {
                NSLog(@"Cannot create subClass of Delegate, beacause the created subClass is not the same size. %@", NSStringFromClass(originalClass));
                NSAssert(NO, @"Classes must be the same size to swizzle isa");
                return;
            }
            
            // 将 delegate 对象设置成新创建的子类对象
            objc_registerClassPair(subClass);
        }
        
        if (object_setClass(delegate, subClass)) {
            NSLog(@"SuccessFully create Delegere Proxy automatically.");
        }
        
    }
    

    第三步:修改 UITableView+SensorsData 中 - sensorsdata_setDelegate:方法

    #import "SensrosAnalyticsDynamicDelegate.h"
    
    - (void)sensorsdata_setDelegate:(id<UITableViewDelegate>)delegate {
        
        // 调用原始的设置代理方法
        // [self sensorsdata_setDelegate:delegate];
        
        // 方案一: 方法交换
        // 交换 delegate 对象中的 tableView:didSelectRowAtIndexPath: 方法
    //    [self sensorsdata_swizzleDidSelectRowIndexPathMethodWithDelegate:delegate];
        
        // 方案二:动态子类
        // 调用原始的设置代理方法
        [self sensorsdata_setDelegate:delegate];
        // 设置 delegate 对象的动态子类
        [SensrosAnalyticsDynamicDelegate proxyWithTableViewDelegate:delegate];
    }
    

    第四步:测试验证

    {
      "event" : "$AppClick",
      "time" : 1648807502558,
      "propeerties" : {
        "$model" : "x86_64",
        "$manufacturer" : "Apple",
        "$element_type" : "UITableView",
        "$lib_version" : "1.0.0",
        "$os" : "iOS",
        "$app_version" : "1.0",
        "$screen_name" : "cn.SensorsDataViewController",
        "$os_version" : "15.2",
        "$lib" : "iOS"
      }
    }
    

    问题: "$screen_name"的名称是动态生成子类的名称 "cn.SensorsDataViewController", 我们期望是原类的名称。

    解决方案:在生成的子类中,重写 class 方法,该方法返回原始子类。

    第一步:重写 class 方法

    - (Class)sensorsdata_class {
        // 获取对象的类
        Class class = object_getClass(self);
        // 将类名前缀替换成空字符串,获取原始类名
        NSString *className = [NSStringFromClass(class) stringByReplacingOccurrencesOfString:kSensorsDelegatePrefix withString:@""];
        // 通过字符串获取类,返回
        return objc_getClass(className.UTF8String);
    }
    

    第二步:给动态创建的子类添加 class 方法

    + (void)proxyWithTableViewDelegate:(id<UITableViewDelegate>)delegate {
        SEL originalSelector = NSSelectorFromString(@"tableView:didSelectRowAtIndexPath:");
        // 当 delegate 对象中没有实现 tableView:didSelectRowAtIndexPath: 方法时,直接返回
        if (![delegate respondsToSelector:originalSelector]) {
            return;
        }
        
        // 动态创建一个新类
        Class originalClass = object_getClass(delegate);
        NSString *originalClassName = NSStringFromClass(originalClass);
        // 当 delegate 对象已经是一个动态创建的类时,无需重复创建,,直接返回
        if ([originalClassName hasPrefix:kSensorsDelegatePrefix]) {
            return;
        }
        
        NSString *subClassName = [kSensorsDelegatePrefix stringByAppendingString:originalClassName];
        Class subClass = NSClassFromString(subClassName);
        if (!subClass) {
            // 注册一个新的子类,其父类为originalClass
            subClass = objc_allocateClassPair(originalClass, subClassName.UTF8String, 0);
            
            // 获取 SensrosAnalyticsDynamicDelegate 中的 tableView:didSelecorRowIndexPath: 方法指针
            Method method = class_getInstanceMethod(self, originalSelector);
            // 获取方法实现
            IMP methodIMP = method_getImplementation(method);
            // 获取方法类型编码
            const char *types = method_getTypeEncoding(method);
            // 在 subClass 中添加 tableView:didSelectRowAtIndexPath: 方法
            if (!class_addMethod(subClass, originalSelector, methodIMP, types)) {
                NSLog(@"Cannot copy method to destination selector %@ as it already exists", NSStringFromSelector(originalSelector));
            }
            
            // 获取 SensrosAnalyticsDynamicDelegate 中的 sensorsdata_class 指针
            Method classMethod = class_getInstanceMethod(self, @selector(sensorsdata_class));
            // 获取方法实现
            IMP classIMP = method_getImplementation(classMethod);
            // 获取方法的类型编码
            const char *classTypes = method_getTypeEncoding(classMethod);
            if (!class_addMethod(subClass, @selector(class), classIMP, classTypes)) {
                NSLog(@"Cannot copy method to destination selector -(void)class as it already exists");
            }
            
            // 子类和原始类的大小必须相同 ,不能有更多的成员变量或者属性
            // 如果不同,将导致设置新的子类时,重新分配内存,重写对象的 isa 指针
            if (class_getInstanceSize(originalClass) != class_getInstanceSize(subClass)) {
                NSLog(@"Cannot create subClass of Delegate, beacause the created subClass is not the same size. %@", NSStringFromClass(originalClass));
                NSAssert(NO, @"Classes must be the same size to swizzle isa");
                return;
            }
            
            // 将 delegate 对象设置成新创建的子类对象
            objc_registerClassPair(subClass);
        }
        
        if (object_setClass(delegate, subClass)) {
            NSLog(@"SuccessFully create Delegere Proxy automatically.");
        }
        
    }
    

    第三步:测试验证

    {
      "event" : "$AppClick",
      "time" : 1648808663270,
      "propeerties" : {
        "$model" : "x86_64",
        "$manufacturer" : "Apple",
        "$element_type" : "UITableView",
        "$lib_version" : "1.0.0",
        "$os" : "iOS",
        "$app_version" : "1.0",
        "$screen_name" : "ViewController",
        "$os_version" : "15.2",
        "$lib" : "iOS"
      }
    }
    

    至此,已经通过动态创建子类实现了 UITableView 的 $AppClick 事件。

    1.3 方案三:消息转发

    ​ 在 iOS 应用开发中,自定义一个类的时候,一般都需要继承自 NSObject 类或者 NSObject 的子类。但是 NSProxy 类却并不是继承自 NSObject 类或者 NSObject 的子类,NSProxy 是一个实现了 NSObject 协议的抽象基类。

    实现步骤

    第一步:创建 SensorsAnalyticsDelegateProxy 类,继承 NSProxy, 并添加 + proxyWithTableViewDelegate 类方法

    @interface SensorsAnalyticsDelegateProxy : NSProxy
    
    + (instancetype)proxyWithTableViewDelegate:(id<UITableViewDelegate>) delegate;
    
    @end
    
    @interface SensorsAnalyticsDelegateProxy()
    
    @property (nonatomic, weak) id delegate;
    
    @end
    
    @implementation SensorsAnalyticsDelegateProxy
    
    + (instancetype)proxyWithTableViewDelegate:(id<UITableViewDelegate>) delegate {
        SensorsAnalyticsDelegateProxy *proxy = [SensorsAnalyticsDelegateProxy alloc];
        proxy.delegate = delegate;
        return proxy;
    }
    
    @end
    

    第二步:重写 - methodSignatureForSelector 方法,返回 delegate 对象中对应的方法签名,重写 - forwardInvocation: 方法,将消息转给 delegate 对象执行,并触发 $AppClick 事件

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        // 返回 delegate 对象方法中对应的方法签名
        return [(NSObject *)self.delegate methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        // 先执行 delegate 对象中的方法
        [invocation invokeWithTarget:self.delegate];
        // 判断是否是 cell 的点击事件代理方法
        if (invocation.selector == @selector(tableView:didSelectRowAtIndexPath:)) {
            // 将方法修改成采集数据行为的方法
            invocation.selector = NSSelectorFromString(@"sensorsdata_tableView:didSelectRowAtIndexPath:");
            // 执行是数据采集相关的方法
            [invocation invokeWithTarget:self];
        }
    }
    
    - (void)sensorsdata_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
        [[SensorsAnalyticsSDK sharedInstance] trackAppClickWithTableView:tableView didSelectRowAtIndexPath:indexPath properties:nil];
    }
    

    第三步:修改 UITableView+SensorsData 中 - sensorsdata_setDelegate: 方法,创建委托对象,并设置成 UITableView 控件的 delegate 对象。

    #import "SensorsAnalyticsDelegateProxy.h"
    
    - (void)sensorsdata_setDelegate:(id<UITableViewDelegate>)delegate {
        // 方案三:NSProxy 消息转发
        SensorsAnalyticsDelegateProxy *proxy = [SensorsAnalyticsDelegateProxy proxyWithTableViewDelegate:delegate];
        [self sensorsdata_setDelegate:proxy];
    
    }
    

    第四步:测试验证,程序奔溃。原因是在 - sensorsdata_setDelegate:创建的 proxy 对象是一个临时变量,方法结束后,该对象被销毁。

    解决方法:

    第五步:创建 UIScrollView 的分类 UIScrollView+SensorsData,并在头文件中进行属性声明

    @interface UIScrollView (SensorsData)
    
    @property (nonatomic, strong) SensorsAnalyticsDelegateProxy *sensorsdata_delegateProxy;
    
    @end
    

    第六步:然后,通过 runtime 的 objc_setAssociatedObject 和 objc_getAssociatedObject 函数实现类别中添加属性

    #import <objc/runtime.h>
    
    @implementation UIScrollView (SensorsData)
    
    - (void)setSensorsdata_delegateProxy:(SensorsAnalyticsDelegateProxy *)sensorsdata_delegateProxy {
        objc_setAssociatedObject(self, @selector(setSensorsdata_delegateProxy:), sensorsdata_delegateProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (SensorsAnalyticsDelegateProxy *)sensorsdata_delegateProxy {
        return objc_getAssociatedObject(self, @selector(sensorsdata_delegateProxy));
    }
    
    @end
    

    第七步:修改 - sensorsdata_setDelegate:方法。增加保存委托对象的代码。

    - (void)sensorsdata_setDelegate:(id<UITableViewDelegate>)delegate {
        // 方案三:NSProxy 消息转发
        // 销毁保存的委托对象
        self.sensorsdata_delegateProxy = nil;
        if (delegate) {
            SensorsAnalyticsDelegateProxy *proxy = [SensorsAnalyticsDelegateProxy proxyWithTableViewDelegate:delegate];
            self.sensorsdata_delegateProxy = proxy;
            // 调用原始方法,将代理设置为委托对象
            [self sensorsdata_setDelegate:proxy];
        } else {
            // 调用原始方法,将代理设置nil
            [self sensorsdata_setDelegate:nil];
        }
    }
    

    第八步:测试验证

     {
      "event" : "$AppClick",
      "time" : 1648882043652,
      "propeerties" : {
        "$model" : "x86_64",
        "$manufacturer" : "Apple",
        "$element_type" : "UITableView",
        "$lib_version" : "1.0.0",
        "$os" : "iOS",
        "$app_version" : "1.0",
        "$screen_name" : "ViewController",
        "$os_version" : "15.2",
        "$lib" : "iOS"
      }
    }
    

    1.4 三种方法的总结

    ​ 我们可以通过方法交换、动态子类和消息转发三种方式实现 UITableViewCell 的点击事件。他们各有优缺点。

    方案一:方法交换

    优点:简单,易理解, Method Swizzling 属于成熟技术,性能相对来说比较高。

    缺点:对原始的类有入侵,容易造成冲突。

    方案二:动态子类

    优点:没有对原始的类入侵,不会修改原始类的方法,不会和第三方库冲突,是一种比较稳定的方案。

    缺点:动态创建子类对性能和内存有比较大的消耗。

    方案三:消息转发

    优点:充分利用消息转发机制,对消息进行拦截,性能较好。

    缺点:容易与一些同样使用消息转发进行拦截的第三方库冲突。

    1.5 优化

    (1)获取控件的内容

    大概思路:获取到 UITableViewCell 对象后,递归遍历所有的子控件,每次获取子控件的内容,并按照一定格式进行拼接,然后将拼接的内容作为 UITableViewCell 控件显示的内容。

    第一步:修改 UIView+SensorsData 的 - sensorsdata_elementContent 方法

    - (NSString *)sensorsdata_elementContent {
        // 如果是隐藏控件,不获取控件内容
        if (self.isHidden || self.alpha == 0) {
            return nil;
        }
        // 初始化数组,用于保存子控件的内容
        NSMutableArray *contents = [NSMutableArray array];
        for (UIView *view in self.subviews) {
            // 获取子控件内容
            // 如果子类有内容,例如 UILabel 的 text,获取到的就是 text 属性
            // 如果子类没有内容,将递归调用该方法,获取其子控件的内容
            NSString *content = view.sensorsdata_elementContent;
            if (content.length > 0) {
                // 当该子控件有内容是,保存到数组中
                [contents addObject:content];
            }
        }
        // 当未获取到内容时,返回 nil,如果获取到多个子控件的内容时,使用”-“拼接
        return contents.count == 0 ? nil : [contents componentsJoinedByString:@"-"];
    }
    

    第二步:修改 UIButton 控件的 - sensorsdata_elementContent 方法

    #pragma mark -UIButton
    @implementation UIButton (SensorsData)
    
    - (NSString *)sensorsdata_elementContent {
        return self.currentTitle ?: super.sensorsdata_elementContent;
    }
    
    @end
    

    第三步:修改 UILabel 控件的 - sensorsdata_elementContent 方法

    #pragma mark -UILabel
    @implementation UILabel (SensorsData)
    
    - (NSString *)sensorsdata_elementContent {
        return self.text ?: super.sensorsdata_elementContent;
    }
    
    @end
    

    第四步:修改 SensorsAnalyticsSDK+Track 文件中 - trackAppClickWithTableView:didSelectRowAtIndexPath: properties: 方法

    - (void)trackAppClickWithTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath properties:(nullable NSDictionary <NSString*, id> *)properties {
        
        NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
        // TODO: 获取用户点击的 UITableViewCell 控件对象
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        // TODO: 设置被用户点击的 UITableViewCell 控件上的内容
        eventProperties[@"$element_content"] = cell.sensorsdata_elementContent;
        // TODO: 设置被用户点击 UITableViewCell 控件所在的位置
        
        // 添加自定义属性
        [eventProperties addEntriesFromDictionary:properties];
        [[SensorsAnalyticsSDK sharedInstance] trackAppClickWithView:tableView properties:eventProperties];
    }
    

    第五步:测试验证:

    {
      "event" : "$AppClick",
      "time" : 1648885587341,
      "propeerties" : {
        "$model" : "x86_64",
        "$manufacturer" : "Apple",
        "$element_type" : "UITableView",
        "$lib_version" : "1.0.0",
        "$os" : "iOS",
        "$element_content" : "CELL",
        "$app_version" : "1.0",
        "$screen_name" : "ViewController",
        "$os_version" : "15.2",
        "$lib" : "iOS"
      }
    }
    
    (2)获取 UITableView 的位置

    通过 indexPath 获取用户点击 cell 的位置。

    - (void)trackAppClickWithTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath properties:(nullable NSDictionary <NSString*, id> *)properties {
        
        NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
        // 获取用户点击的 UITableViewCell 控件对象
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        // 设置被用户点击的 UITableViewCell 控件上的内容
        eventProperties[@"$element_content"] = cell.sensorsdata_elementContent;
        // 设置被用户点击 UITableViewCell 控件所在的位置
        eventProperties[@"$element_position"] = [NSString stringWithFormat:@"%ld:%ld", (long)indexPath.section, (long)indexPath.row];
        // 添加自定义属性
        [eventProperties addEntriesFromDictionary:properties];
        [[SensorsAnalyticsSDK sharedInstance] trackAppClickWithView:tableView properties:eventProperties];
    }
    

    运行 Demo 测试验证

     {
      "event" : "$AppClick",
      "time" : 1648887065273,
      "propeerties" : {
        "$model" : "x86_64",
        "$manufacturer" : "Apple",
        "$element_position" : "0:5",
        "$lib_version" : "1.0.0",
        "$os" : "iOS",
        "$element_content" : "CELL",
        "$element_type" : "UITableView",
        "$app_version" : "1.0",
        "$screen_name" : "ViewController",
        "$os_version" : "15.2",
        "$lib" : "iOS"
      }
    }
    

    二、支持 UICollectionView

    ​ UICollectionView 的 cell 的 $AppClick 全埋点点击事件,整体和 UITableView 类似,同样可以用三种方案实现。此刻,我们用第三种方案消息转发来实现UICollectionView 的 cell 的 $AppClick 全埋点点击事件。

    第一步:在 SensorsAnalyticsSDK+Track 中新增 - trackAppClickWithCollection: didSelectItemAtIndexPath: properties: 方法

    /// 支持 UICollectionView 触发 $AppClick 事件
    /// @param collectionView  触发事件的 tableView 视图
    /// @param indexPath 在 tableView 中点击的位置
    /// @param properties 自定义事件参数
    - (void)trackAppClickWithCollection:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath properties:(nullable NSDictionary <NSString*, id> *)properties;
    
    - (void)trackAppClickWithCollection:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath properties:(nullable NSDictionary <NSString*, id> *)properties {
        
        NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
        // 获取用户点击的 UITableViewCell 控件对象
        UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
        // 设置被用户点击的 UITableViewCell 控件上的内容
        eventProperties[@"$element_content"] = cell.sensorsdata_elementContent;
        // 设置被用户点击 UITableViewCell 控件所在的位置
        eventProperties[@"$element_position"] = [NSString stringWithFormat:@"%ld:%ld", (long)indexPath.section, (long)indexPath.row];
        // 添加自定义属性
        [eventProperties addEntriesFromDictionary:properties];
        [[SensorsAnalyticsSDK sharedInstance] trackAppClickWithView:collectionView properties:eventProperties];
    }
    

    第二步:在 SensorsAnalyticsDelegateProxy 中新增初始化方法

    @interface SensorsAnalyticsDelegateProxy : NSProxy
    
    /// 初始化委托对象,用于拦截 UICollectionView 控件选中 cell 事件
    /// @param delegate UICollectionView 控件代理
    + (instancetype)proxyWithCollectionViewDelegate:(id<UICollectionViewDelegate>) delegate;
    
    @end
    
    + (instancetype)proxyWithCollectionViewDelegate:(id<UICollectionViewDelegate>) delegate {
        SensorsAnalyticsDelegateProxy *proxy = [SensorsAnalyticsDelegateProxy alloc];
        proxy.delegate = delegate;
        return proxy;
    }
    

    第三步:修改 - forwardInvocation:方法

    - (void)forwardInvocation:(NSInvocation *)invocation {
        // 先执行 delegate 对象中的方法
        [invocation invokeWithTarget:self.delegate];
        // 判断是否是 cell 的点击事件代理方法
        if (invocation.selector == @selector(tableView:didSelectRowAtIndexPath:)) {
            // 将方法修改成采集数据行为的方法
            invocation.selector = NSSelectorFromString(@"sensorsdata_tableView:didSelectRowAtIndexPath:");
            // 执行是数据采集相关的方法
            [invocation invokeWithTarget:self];
        } else if (invocation.selector == @selector(collectionView:didSelectItemAtIndexPath:)) {
            // 将方法修改成采集数据行为的方法
            invocation.selector = NSSelectorFromString(@"sensorsdata_collectionView:didSelectItemAtIndexPath:");
            // 执行是数据采集相关的方法
            [invocation invokeWithTarget:self];
        }
    }
    
    - (void)sensorsdata_collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
        [[SensorsAnalyticsSDK sharedInstance] trackAppClickWithCollection:collectionView didSelectItemAtIndexPath:indexPath properties:nil];
    }
    

    第四步:新增 UICollectionView 类别 UICollectionView+SensorsData,实现 + load 方法交换和设置代理对象

    + (void)load {
        [UICollectionView sensorsdata_swizzleMethod:@selector(setDelegate:) withMethod:@selector(sensorsdata_setDelegate:)];
    }
    
    - (void)sensorsdata_setDelegate:(id<UICollectionViewDelegate>) delegate {
        // NSProxy 消息转发
        // 销毁保存的委托对象
        self.sensorsdata_delegateProxy = nil;
        if (delegate) {
            SensorsAnalyticsDelegateProxy *proxy = [SensorsAnalyticsDelegateProxy proxyWithCollectionViewDelegate:delegate];
            self.sensorsdata_delegateProxy = proxy;
            // 调用原始方法,将代理设置为委托对象
            [self sensorsdata_setDelegate:proxy];
        } else {
            // 调用原始方法,将代理设置nil
            [self sensorsdata_setDelegate:nil];
        }
    }
    

    第五步:测试验证

  • 相关阅读:
    c#查找窗口的两种办法
    也说自动化测试
    定位bug的基本要求
    c#调用GetModuleFileNameEx获取进程路径
    对比PG数据库结构是否一致的方法
    C#调用endtask
    提bug
    接口测试的结果校验
    ProcessExplorer使用分享
    C++如何在r3静态调用NT函数
  • 原文地址:https://www.cnblogs.com/r360/p/16128674.html
Copyright © 2020-2023  润新知