iOS 全埋點-控件點擊事件(3)

寫在前面

傳送門:

前面的系列章節(jié)可以查看上面連接,本章節(jié)主要是介紹 iOS全埋點序列文章(3)控件點擊事件分析

Target-Action設(shè)計模式

在具體介紹如何實現(xiàn)之前,我們需要先了解在UIKit框架下點擊或拖動 事件的Target-Action設(shè)計模式。
Target-Action模式主要包含兩個部分。

  • Target(對象):接收消息的對象。
  • Action(方法):用于表示需要調(diào)用的方法

Target可以是任意類型的對象。但是在iOS應(yīng)用程序中,通常情況下會 是一個控制器,而觸發(fā)事件的對象和接收消息的對象(Target)一樣,也可 以是任意類型的對象。例如,手勢識別器UIGestureRecognizer就可以在識 別到手勢后,將消息發(fā)送給另一個對象。

當我們?yōu)橐粋€控件添加Target-Action后,控件又是如何找到Target并執(zhí) 行對應(yīng)的Action的呢?

UIControl類中有一個方法:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;

用戶操作控件(比如點擊)時,首先會調(diào)用這個方法,并將事件轉(zhuǎn)發(fā) 給應(yīng)用程序的UIApplication對象。

同時,在UIApplication類中也有一個類似的實例方法:
- (BOOL)sendAction:(SEL)action to:(nullable id)target from:(nullable id)sender forEvent:(nullable UIEvent *)event;

如果Target不為nil,應(yīng)用程序會讓該對象調(diào)用對應(yīng)的方法響應(yīng)事件;如果Targetnil,應(yīng)用程序會在響應(yīng)鏈中搜索定義了該方法的對象,然后 執(zhí)行該方法。

基于Target-Action設(shè)計模式,有兩種方案可以實現(xiàn)$AppClick事件的全埋點。下面我們將逐一進行介紹。

方案一

描述

通過Target-Action設(shè)計模式可知,在執(zhí)行Action之前,會先后通過控件 和UIApplication對象發(fā)送事件相關(guān)的信息。因此,我們可以通過Method Swizzling交換UIApplication類中的-sendAction:to:from:forEvent:方法,然后 在交換后的方法中觸發(fā)$AppClick事件,并根據(jù)targetsender采集相關(guān)屬性,實現(xiàn)$AppClick事件的全埋點。

代碼實現(xiàn)

新建一個UIApplication的分類

+ (void)load {
    [UIApplication sensorsdata_swizzleMethod:@selector(sendAction:to:from:forEvent:) withMethod:@selector(CountData_sendAction:to:from:forEvent:)];
}

- (BOOL)CountData_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event {
    [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:nil];
    return  [self CountData_sendAction:action to:target from:sender forEvent:event];
}

一般情況下,對于一個控件的點擊事件,我們至少還需要采集如下信息(屬性):

  • 控件類型($element_type
  • 控件上顯示的文本($element_content
  • 控件所屬頁面($screen_name

獲取控件類型

先為你介紹一下NSObject對象的繼承關(guān)系圖

NSObject的體系

從上圖可以看出,控件都是繼承于UIView,所以獲取要想獲取控件類型,可以聲明UIView的分類

新建UIView的分類(UIView+TypeData)

UIView+TypeData.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIView (TypeData)

@property (nonatomic,copy,readonly) NSString *elementType;

@end

NS_ASSUME_NONNULL_END

UIView+TypeData.m

#import "UIView+TypeData.h"

@implementation UIView (TypeData)

- (NSString *)elementType {
    return  NSStringFromClass([self class]);
}
@end

獲取控件類型的埋點實現(xiàn)

+ (void)load {
    [UIApplication sensorsdata_swizzleMethod:@selector(sendAction:to:from:forEvent:) withMethod:@selector(CountData_sendAction:to:from:forEvent:)];
}

- (BOOL)CountData_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event {
    UIView *view = (UIView *)sender;
    NSMutableDictionary *prams = [[NSMutableDictionary alloc]init];
    //獲取控件類型
    prams[@"$elementtype"] = view.elementType;
    [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:prams];
    return  [self CountData_sendAction:action to:target from:sender forEvent:event];
}

獲取顯示的文本

獲取顯示的文本,我們只需要針對特定的控件,調(diào)用相應(yīng)的方法即可。我們以UIButton為例來介紹實現(xiàn)步驟。
首先聲明一個UIView的分類UIView+TextContentData,然后在UIView的分類UIView+TextContentData添加 UIButton的分類
UIButton的分類。

UIView+TextContentData.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIView (TextContentData)
@property (nonatomic,copy,readonly) NSString *elementContent;
@end

@interface UIButton (TextContentData)

@end

NS_ASSUME_NONNULL_END

UIView+TextContentData.m

#import "UIView+TextContentData.h"

@implementation UIView (TextContentData)

- (NSString *)elementContent {
    return  nil;
}

@end

@implementation  UIButton (TextContentData)

- (NSString *)elementContent {
    return self.titleLabel.text;
}

@end

獲取控件的文本埋點實現(xiàn)

+ (void)load {
    [UIApplication sensorsdata_swizzleMethod:@selector(sendAction:to:from:forEvent:) withMethod:@selector(CountData_sendAction:to:from:forEvent:)];
}

- (BOOL)CountData_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event {
    UIView *view = (UIView *)sender;
    NSMutableDictionary *prams = [[NSMutableDictionary alloc]init];
    //獲取控件類型
    prams[@"$elementtype"] = view.elementType;
    prams[@"element_content"] = view.elementContent;
    [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:prams];
    return  [self CountData_sendAction:action to:target from:sender forEvent:event];
}

我們這里只是以UIButton為例,如果想擴充其他控件,直接添加對應(yīng)控件的分類。

獲取控件所屬頁面

如何知道UIView屬于那個UIViewController,這個就需要借助UIResponder了。

UIApplicationUIViewController、UIView類都是UIResponder的子類,在iOS應(yīng)用程序中,UIApplication、 UIViewController、UIView類的對象也都是響應(yīng)者,這些響應(yīng)者會形成一個 響應(yīng)者鏈。

一個完整的響應(yīng)者鏈傳遞規(guī)則(順序)大概如下: UIViewUIViewControllerUIWindowUIApplicationUIApplicationDelegate
如下圖所示:

響應(yīng)者鏈

通過響應(yīng)鏈圖可知,對于任意一個視圖來說,都能通過響應(yīng)者鏈找到它所 在的視圖控制器,也就是其所屬的頁面,從而達到獲取所屬頁面信息的目 的。

注意:對于在iOS應(yīng)用程序中實現(xiàn)了UIApplicationDelegate協(xié)議的類(通常為AppDelegate),如果它是繼承自UIResponder,那么也會參與響應(yīng)者 鏈的傳遞;如果不是繼承自UIResponder(例如NSObject),那么不會參與響應(yīng)者鏈的傳遞。

UIView+TextContentData.h

@interface UIView (TextContentData)

@property (nonatomic,copy,readonly) NSString *elementContent;
@property (nonatomic,strong,readonly) UIViewController *myViewController;

@end

UIView+TextContentData.m

#import "UIView+TextContentData.h"

@implementation UIView (TextContentData)

- (NSString *)elementContent {
    return  nil;
}

- (UIViewController *)myViewController {
    UIResponder *responder = self;
    while ((responder = [responder nextResponder])) {
        if ([responder isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)responder;
        }
    }
    return  nil;
}

@end

獲取控件所屬頁面埋點實現(xiàn)

+ (void)load {
    [UIApplication sensorsdata_swizzleMethod:@selector(sendAction:to:from:forEvent:) withMethod:@selector(CountData_sendAction:to:from:forEvent:)];
}

- (BOOL)CountData_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event {
    UIView *view = (UIView *)sender;
    NSMutableDictionary *prams = [[NSMutableDictionary alloc]init];
    //獲取控件類型
    prams[@"$elementtype"] = view.elementType;
    //獲取控件的內(nèi)容
    prams[@"element_content"] = view.elementContent;
    //獲取所屬的頁面
    UIViewController *vc = view.myViewController;
    prams[@"element_screen"] = NSStringFromClass(vc.class);
    [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:prams];

    return  [self CountData_sendAction:action to:target from:sender forEvent:event];
}

更多控件

支持獲取UISwitch控件文本信息

通過測試可以發(fā)現(xiàn),UISwitch$AppClick事件沒有$element_content屬性。針對這個問題,可以解釋為UISwitch控件本身就沒有顯示任何文本。 為了方便分析,針對獲取UISwitch控件的文本信息,我們可以定一個簡單的規(guī)則:當UISwitch控件的on屬性為YES時,文本為“checked”;當 UISwitch控件的on屬性為NO時,文本為“unchecked”。

解決方案
聲明 UISwitch的分類

@implementation UISwitch (TextContentData)

- (NSString *)elementContent {
    return self.on ? @"checked":@"unchecked";
}

@end

滑動UISlider控件重復觸發(fā)$AppClick事件解決方案

原因
我們在滑動UISlider控件過程中,系統(tǒng)會依次觸發(fā) UITouchPhaseBeganUITouchPhase-Moved、UITouchPhaseMoved、……、 UITouchPhaseEnded事件,而每一個事件都會觸發(fā)UIApplication- sendAction:to:from:forEvent:方法執(zhí)行,從而觸發(fā)$AppClick事件。
防止滑動UISlider重復響應(yīng),只有在UITouchPhaseEnded開始響應(yīng)

 //防止滑動UISlider控制
    if(event.allTouches.anyObject.phase == UITouchPhaseEnded || [sender isKindOfClass:[UISwitch class]]) {
        [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:prams];
    }

方案二

描述

當一個視圖被添加到父視圖上時,系統(tǒng)會自 動調(diào)用-didMoveToSuperview方法。因此,我們可 以通過Method Swizzling交換UIView- didMoveToSuperview方法,然后在交換方法里給 控件添加一組UIControlEventTouchDown類型的 Target-Action,并在Action里觸發(fā)$AppClick事 件,從而實現(xiàn)$AppClick事件全埋點,這就是方案二的實現(xiàn)原理。

代碼實現(xiàn)

新建一個UIControl的分類

UIControl+CountData.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIControl (CountData)

@end

NS_ASSUME_NONNULL_END

UIControl+CountData.m

+ (void)load {
    
    [UIControl sensorsdata_swizzleMethod:@selector(didMoveToSuperview) withMethod:@selector(CountData_didMoveToSuperview)];
}

- (void)CountData_didMoveToSuperview {
    
    //調(diào)用前交換原始方法
    [self CountData_didMoveToSuperview];
    [self addTarget:self action:@selector(CountData_touchDownAction:withEvent:) forControlEvents:UIControlEventTouchDown];

}

-(void)CountData_touchDownAction:(UIControl *)sender withEvent:(UIEvent *)event {
    if ([self CountData_isAddMultipleTargetActionsWithDefaultEvent:UIControlEventTouchDown]) {
        //觸發(fā)$AppClick事件
        UIView *view = (UIView *)sender;
        NSMutableDictionary *prams = [[NSMutableDictionary alloc]init];
        //獲取控件類型
        prams[@"$elementtype"] = view.elementType;
        //獲取控件的內(nèi)容
        prams[@"element_content"] = view.elementContent;
        //獲取所屬的頁面
        UIViewController *vc = view.myViewController;
        prams[@"element_screen"] = NSStringFromClass(vc.class);
          
        [[SensorsAnalyticsSDK sharedInstance]track:@"appclick" properties:prams];
    }
}

注意點UIControl類中其實并沒有實現(xiàn)-didMoveToSuperview方法,這個方法是 從它的父類UIView繼承而來的。因此,我們實際上交換的是UIView中的- didMoveToSuperview方法。當UIView對象調(diào)用-didMoveToSuperview方法時,其實調(diào)用的是在UIControl+CountData.m中實現(xiàn)的- CountData_didMoveToSuperview方法。但是,UIView對象或者除了 UIControl類的其他UIView子類的對象,在執(zhí)行-CountData_didMoveToSuperview方法時,并沒有實現(xiàn)-CountData_didMoveToSuperview方法,因此,程序會出現(xiàn) 找不到方法而崩潰的情況。

針對這個問題,我們需要修改NSObject+SASwizzler.m文件中的 +sensorsdata_swizzleMethod:withMethod:類方法,即將其修改為:在方法交換之前,先在當前類中添加需要交換的方法,并在添加成功之后獲取新的方法指針。

+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL {
   
    //獲取原始的方法
    Method originalMethod = class_getInstanceMethod(self, originalSEL);
    if (!originalMethod) {
        return NO;
    }
    //獲取將要交換的方法
    Method alternateMethod = class_getInstanceMethod(self, alternateSEL);
    if (!alternateMethod) {
        return NO;
    }
    
    //獲取originalSel方法實現(xiàn)
    IMP originalIMP = method_getImplementation(originalMethod);
    //獲取originalSEL方法的類型
    const char *originalMethodType = method_getTypeEncoding(originalMethod);
    //往類中添加originalSEL方法,如果已經(jīng)存在,則添加失敗,并返回NO
    if (class_addMethod(self, originalSEL, originalIMP, originalMethodType)) {
        //如果添加成功,重新獲取originalSEL實例方法
        originalMethod = class_getInstanceMethod(self, originalSEL);
    }

    //獲取alternateIMP方法實現(xiàn)
    IMP alternateIMP = method_getImplementation(alternateMethod);
    //獲取alternateSEL方法的類型
    const char *alternateMethodType = method_getTypeEncoding(alternateMethod);
    //往類中添加alternateSEL方法,如果已經(jīng)存在,則添加失敗,并返回NO
    if (class_addMethod(self, alternateSEL, alternateIMP, alternateMethodType)) {
        //如果添加成功,重新獲取alternateSEL實例方法
        alternateMethod = class_getInstanceMethod(self, alternateSEL);
    }

    //交互兩個方法的實現(xiàn)
    method_exchangeImplementations(originalMethod, alternateMethod);  
    //返回yes,方法交換成功
    return YES;
}

支持更多控件

支持UISwitch、UISegmentedControl、UIStepper控件

這些控件都不響應(yīng)UIControlEventTouchDown類型的Action,也就是說,沒有觸發(fā)-sensorsdata_touchDownAction:event:方法,因此,也就不會觸發(fā)$AppClick事件。實際上,這些控件添加的是 UIControlEventValueChanged類型的Action。

+ (void)load { 
    [UIControl sensorsdata_swizzleMethod:@selector(didMoveToSuperview) withMethod:@selector(CountData_didMoveToSuperview)];
}

- (void)CountData_didMoveToSuperview {
    
    //調(diào)用前交換原始方法
    [self CountData_didMoveToSuperview];
    //判斷是否為一些特殊的控件
    if([self isKindOfClass:[UISwitch class]] ||
       [self isKindOfClass:[UISegmentedControl class]] ||
       [self isKindOfClass:[UIStepper class]] 
     ) {
        [self addTarget:self action:@selector(countData_valueChangedAction:event:) forControlEvents:UIControlEventValueChanged];
    }else {
        [self addTarget:self action:@selector(CountData_touchDownAction:withEvent:) forControlEvents:UIControlEventTouchDown];
    }
}

-(void)countData_valueChangedAction:(UIControl *)sender event:(UIEvent *)event {
    
    if ([self CountData_isAddMultipleTargetActionsWithDefaultEvent:UIControlEventValueChanged]) {    
        [[SensorsAnalyticsSDK sharedInstance]track:@"appclick" properties:nil];
    }
    
}

-(BOOL)CountData_isAddMultipleTargetActionsWithDefaultEvent:(UIControlEvents)defaultEvent {
    ///如果有多個target,說明除了添加的target,還有其他
    ///那么返回YES,觸發(fā)$AppClick事件
    if (self.allTargets.count > 2) {
        return YES;
    }
    
    //如果控件本身為target,并且添加了不是UIControlEventTouchDown類型的Action
    //說明開發(fā)者以控件本身為target,并且已添加添加Action
    //那么返回YES,觸發(fā)$AppClick事件
    if((self.allControlEvents & UIControlEventAllEvents) != UIControlEventTouchDown) {
        return YES;
    }
    
    //如果控件本身為Target,并且添加了兩個以上的UIControlEventTouchDown類型的Action
    //說明開發(fā)者自行添加了Action
    //那么返回YES,觸發(fā)$AppClick事件
    if([self actionsForTarget:self forControlEvent:defaultEvent].count > 2) {
        return YES;
    }

    return NO;
    
}

支持UISlider控件

UISlider添加的是UIControlEventTouchDown 類型的Action,這會導致在只點擊而沒有滑動UISlider時,也會觸發(fā) $AppClick事件,我們更希望只有手停止滑動UISlider時,才觸發(fā)$AppClick事件。因此,需要修改UIControl+SensorsData.m文件中的- sensorsdata_didMoveToSuperview方法,默認也給UISlider添加UIControlEventValueChanged類型的Action。

- (void)CountData_didMoveToSuperview {
    
    //調(diào)用前交換原始方法
    [self CountData_didMoveToSuperview];
    //判斷是否為一些特殊的控件
    if([self isKindOfClass:[UISwitch class]] ||
       [self isKindOfClass:[UISegmentedControl class]] ||
       [self isKindOfClass:[UIStepper class]] ||
       [self isKindOfClass:[UISlider class]]) {
        [self addTarget:self action:@selector(countData_valueChangedAction:event:) forControlEvents:UIControlEventValueChanged];
    }else {
        [self addTarget:self action:@selector(CountData_touchDownAction:withEvent:) forControlEvents:UIControlEventTouchDown];
    }
}

在滑動UISlider過程中,會一直觸發(fā)$AppClick事件。因此,我們還需要修改UIControl+CountData.m文件中 的-CountData_valueChanged Action:event:方法,確保如果是UISlider控件, 只有在手抬起的時候才觸發(fā)$AppClick事件。

-(void)countData_valueChangedAction:(UIControl *)sender event:(UIEvent *)event {
    
    if ([sender isKindOfClass:UISlider.class] && event.allTouches.anyObject.phase != UITouchPhaseEnded) {
        return;
    }
    
    if ([self CountData_isAddMultipleTargetActionsWithDefaultEvent:UIControlEventValueChanged]) {  
        [[SensorsAnalyticsSDK sharedInstance]track:@"appclick" properties:nil];
    }
    
}


這樣處理之后,當我們滑動UISlider時,只會在手抬起時觸發(fā) $AppClick事件。

方案總結(jié)

方案一和方案二其實都運用了iOS中的Target- Action模式,這兩種方案各有優(yōu)劣。

  • 對于方案一:如果給一個控件添加了多個 Target-Action,會導致多次觸發(fā)$AppClick事件。
  • 對于方案二:由于SDK為控件添加了一個默認觸發(fā)類型的Action,因此,如果開發(fā)者在開發(fā) 過程中使用UIControl類的allTargets或者 allControlEvents屬性進行邏輯判斷,有可能會引入一些無法預料的問題。 因此,在選擇方案的時候,讀者可以根據(jù)自 己的實際情況和需求,來確定最終的實現(xiàn)方案。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文主要講解iOS觸摸事件的一系列機制,涉及的問題大致包括: 觸摸事件由觸屏生成后如何傳遞到當前應(yīng)用? 應(yīng)用接收觸...
    baihualinxin閱讀 1,275評論 0 9
  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?困惑于Cell怎么突然不能點擊了?糾結(jié)于如何實現(xiàn)這個奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 59,510評論 51 604
  • 一.觸摸、事件、響應(yīng)者、手勢、UIControl 1. UITouch 一個手指對應(yīng)一個UITouch對象,多個手...
    hello_iOS程序媛閱讀 2,486評論 0 1
  • 0、緣起 之所以要寫這篇文章,是因為發(fā)現(xiàn)在實際編程處理點擊事件的過程中,知道響應(yīng)鏈和探測鏈根本沒有一點用處。 即使...
    吳佩在天涯閱讀 44,725評論 33 127
  • 問題:給一個UIbutton 添加手勢 和添加touchUpInside 和 重寫touch:begin方法,誰...
    平常心_kale閱讀 403評論 0 0

友情鏈接更多精彩內(nèi)容