iOS全埋點解決方案-UITableView和UICollectionView點擊事件

来源:https://www.cnblogs.com/r360/archive/2022/04/11/16128674.html
-Advertisement-
Play Games

前言 在 $AppClick 事件採集中,還有兩個比較特殊的控制項: UITableView •UICollectionView 這兩個控制項的點擊事件,一般指的是點擊 UITableViewCell 和 UICollectionViewCell。而 UITableViewCell 和 UICollec ...


前言

在 $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];
    }
}

第五步:測試驗證


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、指針函數 指針函數,從名字上看它本質上是一個函數。指針函數:返回值類型是指針的函數。函數聲明如下: int *plusfunction(int a,int b); 當然也可以寫成如下格式: int* plusfunction(int a,int b); 讓指針標誌 * 與int緊貼在一起,而與函 ...
  • 什麼雲主機既能隨時自助獲取、可彈性伸縮,價格還不貴,一年只要39元,那必定就是華為雲主機,因為其好的售後體驗,華為雲獲得可信雲電商雲服務獎,雲主機獲五星+最高評級。下麵我們來瞭解下華為雲主機吧。 華為云云主機介紹 華為云云主機是一種彈性雲伺服器(Elastic Cloud Server, ECS), ...
  • 企業雲伺服器怎麼選,要安全、靈活,還有容量大,可以試試這款華為云云主機,華為的技術一直是國內天花板級別的,所以你可以相信華為云云主機的性能。下麵對此做個評測: 企業在選擇華為雲主機遇到的問題: 很多企業會在多個雲平臺部署業務,一來享受不同雲廠商的產品和服務優勢;二來分散和減少業務系統風險。但多雲部署 ...
  • 雲主機作為雲計算最基礎、最核心的產品,承擔了大部分企業的計算任務,其性能和穩定性直接決定了雲計算的用戶體驗。 眾所周知,雲計算從來不是科技的狂人妄語,在科技技術飛速發展的時代,在技術市場,我們目睹了這個產業從零到數千億美元,黃金白銀的背後都來自產業的真實需求。尤其是突如其來的疫情,雖然讓很多產業均受 ...
  • Linux進程通訊機制 Linux 系統中有萬物皆文件的說法,虛擬文件系統(VFS)是 Linux 對外的介面,任何程式都必須通過這層介面來使用它。 為了避免系統安全問題(越權訪問),進程間記憶體無法共用,數據交互就得採用特殊的通信機制(IPC)。 進程劃分用戶空間(不可共用)跟內核空間(可共用),並 ...
  • 前言 為什麼要老藥換新湯 作為Android中 至關重要 的機制之一,十多年來,分析它的文章不斷,大量的內容已經被挖掘過了。所以: 已經對這一機制熟稔於心的讀者,在這篇文章中,看不到新東西了。 但對於還不太熟悉消息機制的讀者,可以在文章的基礎上,繼續挖一挖。 一般,諸如此類有關Android的消息機 ...
  • 所有OpenHarmony相關官方直播課程,我們都將在OpenHarmony B站官方賬號“OpenHarmony開發者社區”上彙總發佈。 ...
  • 為了適配更多移動產品形態、提升用戶體驗、以及與合作伙伴協同促進生態建設持續高品質發展,我們本次對《原子化服務上架規範》(後文簡稱:規範)做了更新。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...