前言
在 $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];
}
}
第五步:測試驗證