iOS full buried point - page control click event

Keywords: iOS

Write in front

Portal:

Target action design pattern

Before specifically introducing how to implement it, we need to understand the target action design pattern of click or drag events under the UIKit framework.
The target action pattern mainly consists of two parts.

  • Target: the object that receives the message.
  • Action (method): used to represent the method to be called

Target can be any type of object. However, in iOS applications, it is usually a controller, and the object that triggers the event, like the object that receives the message (target), can also be any type of object. For example, the gesture recognizer UIGestureRecognizer can send the message to another object after recognizing the gesture.

When we add a Target Action to a control, how does the control find the Target and execute the corresponding Action?

There is a method in the UIControl class:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;

When the user operates the control (such as clicking), this method will be called first and the event will be forwarded to the UIApplication object of the application.

At the same time, there is a similar instance method in UIApplication class:
- (BOOL)sendAction:(SEL)action to:(nullable id)target from:(nullable id)sender forEvent:(nullable UIEvent *)event;

If the Target is not nil, the application will ask the object to call the corresponding method to respond to the event; if the Target is nil, the application will search the response chain for the object with the method defined, and then execute the method.

Based on the target action design pattern, there are two schemes to realize the full burial point of $AppClick events. We will introduce them one by one.

Scheme I

describe

According to the target Action design pattern, event related information will be sent through the control and UIApplication object before the Action is executed. Therefore, we can Method Swizzling Exchange the - sendAction:to:from:forEvent: Method in the UIApplication class, then trigger the $AppClick event in the exchanged method, and collect relevant attributes according to the target and sender to realize the full embedding point of the $AppClick event.

code implementation

Create a new category of 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];
}

Generally, for the click event of a control, we need to collect at least the following information (properties):

  • Control type ($element_type)
  • Control ($element_content)
  • Page to which the control belongs ($screen_name)

Get control type

First, let me introduce the inheritance diagram of NSObject object


As can be seen from the above figure, all controls inherit from UIView, so to obtain the control type, you can declare the classification of UIView

Create a new category for 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

Gets the embedded point implementation of the control type

+ (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];
    //Get control type
    prams[@"$elementtype"] = view.elementType;
    [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:prams];
    return  [self CountData_sendAction:action to:target from:sender forEvent:event];
}

Gets the displayed text

To get the displayed text, we only need to call the corresponding method for a specific control. Let's take UIButton as an example to introduce the implementation steps.
First declare the classification UIView+TextContentData of a UIView, and then add the classification of UIButton in the classification UIView+TextContentData of UIView
Classification of uibuttons.

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

Get the text embedding point implementation of the control

+ (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];
    //Get control type
    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];
}

Here we just take UIButton as an example. If you want to expand other controls, you can directly add the classification of corresponding controls.

Gets the page to which the control belongs

How to know which UIViewController the UIView belongs to needs the help of UIResponder.

UIApplication, UIViewController and UIView classes are subclasses of UIResponder. In iOS applications, the objects of UIApplication, UIViewController and UIView classes are also responders. These responders will form a responder chain.

A complete responder chain transfer rule (order) is roughly as follows: UIView → UIViewController → UIWindow → UIApplication → UIApplicationDelegate
As shown in the figure below:

According to the response chain diagram, for any view, the view controller, that is, the page to which it belongs, can be found through the responder chain, so as to obtain the page information.

Note: for a class (usually AppDelegate) that implements UIApplicationDelegate protocol in iOS applications, if it inherits from UIResponder, it will also participate in the transmission of responder chain; if it does not inherit from UIResponder (such as NSObject), it will not participate in the transmission of responder chain.

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

Get the embedded point implementation of the page to which the control belongs

+ (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];
    //Get control type
    prams[@"$elementtype"] = view.elementType;
    //Gets the content of the control
    prams[@"element_content"] = view.elementContent;
    //Get the page to which you belong
    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];
}

More controls

Support obtaining text information of UISwitch control

Through the test, it can be found that the $AppClick event of the UISwitch does not have the $element_content attribute. To solve this problem, it can be explained that the UISwitch control itself does not display any text. To facilitate analysis, we can set a simple rule for obtaining the text information of the UISwitch control: when the on attribute of the UISwitch control is YES, the text is "checked" ; when the on property of the UISwitch control is NO, the text is "unchecked".

Solution
Declare the classification of the UISwitch

@implementation UISwitch (TextContentData)

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

@end

Sliding UISlider control repeatedly triggers $AppClick event solution

reason:
During the process of sliding the UISlider control, the system will trigger UITouchPhaseBegan, uitouchphase moved, UITouchPhaseMoved,..., and UITouchPhaseEnded events in turn, and each event will trigger the execution of the - sendAction:to:from:forEvent: method of UIApplication, thus triggering the $AppClick event.
Prevent the sliding UISlider from repeating the response. The response starts only when UITouchPhaseEnded

 //Prevent sliding UISlider control
    if(event.allTouches.anyObject.phase == UITouchPhaseEnded || [sender isKindOfClass:[UISwitch class]]) {
        [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:prams];
    }

Scheme II

describe

When a view is added to the parent view, the system will automatically call the - didMoveToSuperview method. Therefore, we can Method Swizzling Exchange the - didMoveToSuperview method of UIView, then add a group of target actions of UIControlEventTouchDown type to the control in the exchange method, and trigger the $AppClick event in the Action, so as to realize the full burial point of $AppClick event. This is the implementation principle of scheme 2.

code implementation

Create a new category of 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 {
    
    //Swap original methods before calling
    [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]) {
        //Trigger $AppClick event
        UIView *view = (UIView *)sender;
        NSMutableDictionary *prams = [[NSMutableDictionary alloc]init];
        //Get control type
        prams[@"$elementtype"] = view.elementType;
        //Gets the content of the control
        prams[@"element_content"] = view.elementContent;
        //Get the page to which you belong
        UIViewController *vc = view.myViewController;
        prams[@"element_screen"] = NSStringFromClass(vc.class);
          
        [[SensorsAnalyticsSDK sharedInstance]track:@"appclick" properties:prams];
    }
}

Note: the UIControl class does not actually implement the - didMoveToSuperview method, which is inherited from its parent class UIView. Therefore, what we actually exchange is the - didMoveToSuperview method in UIView. When the UIView object calls the - didMoveToSuperview method, it actually calls - countdata implemented in UIControl+CountData.m_ The didMoveToSuperview method. However, UIView objects or objects of other UIView subclasses other than UIControl class are executing - countdata_ The didMoveToSuperview method does not implement - CountData_didMoveToSuperview method, so the program will crash when the method is not found.

To solve this problem, we need to modify the + sensorsdata in the NSObject+SASwizzler.m file_ Swizzlemethod: WITHMETHOD: class method, which is modified to: before method exchange, add the method to be exchanged in the current class, and obtain a new method pointer after the addition is successful.

+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL {
   
    //Get the original method
    Method originalMethod = class_getInstanceMethod(self, originalSEL);
    if (!originalMethod) {
        return NO;
    }
    //Gets the method to be exchanged
    Method alternateMethod = class_getInstanceMethod(self, alternateSEL);
    if (!alternateMethod) {
        return NO;
    }
    
    //Get the originalSel method implementation
    IMP originalIMP = method_getImplementation(originalMethod);
    //Gets the type of the originalSEL method
    const char *originalMethodType = method_getTypeEncoding(originalMethod);
    //Add the originalSEL method to the class. If it already exists, the addition fails and returns NO
    if (class_addMethod(self, originalSEL, originalIMP, originalMethodType)) {
        //If the addition is successful, retrieve the originalSEL instance method
        originalMethod = class_getInstanceMethod(self, originalSEL);
    }

    //Get alternateIMP method implementation
    IMP alternateIMP = method_getImplementation(alternateMethod);
    //Gets the type of the alternateSEL method
    const char *alternateMethodType = method_getTypeEncoding(alternateMethod);
    //Add the alternateSEL method to the class. If it already exists, the addition fails and returns NO
    if (class_addMethod(self, alternateSEL, alternateIMP, alternateMethodType)) {
        //If the addition is successful, re obtain the alternateSEL instance method
        alternateMethod = class_getInstanceMethod(self, alternateSEL);
    }

    //Implementation of two interactive methods
    method_exchangeImplementations(originalMethod, alternateMethod);  
    //Returns yes, and the method exchange is successful
    return YES;
}

Support more controls

Support UISwitch, UISegmentedControl and UIStepper controls

These controls do not respond to UIControlEventTouchDown type actions, that is, they do not trigger - sensordata_ Touchdownaction: Event: method, so the $AppClick event will not be triggered. In fact, these controls add actions of type UIControlEventValueChanged.

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

- (void)CountData_didMoveToSuperview {
    
    //Swap original methods before calling
    [self CountData_didMoveToSuperview];
    //Determine whether it is a special control
    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 {
    ///If there are multiple targets, it indicates that there are other targets besides the added targets
    ///Then return YES to trigger the $AppClick event
    if (self.allTargets.count > 2) {
        return YES;
    }
    
    //If the control itself is a target and an Action of type other than UIControlEventTouchDown is added
    //It indicates that the developer takes the control itself as the target and has added an Action
    //Then return YES to trigger the $AppClick event
    if((self.allControlEvents & UIControlEventAllEvents) != UIControlEventTouchDown) {
        return YES;
    }
    
    //If the control itself is a Target and more than two UIControlEventTouchDown type actions are added
    //This indicates that the developer has added an Action
    //Then return YES to trigger the $AppClick event
    if([self actionsForTarget:self forControlEvent:defaultEvent].count > 2) {
        return YES;
    }

    return NO;
    
}

Support for UISlider controls

An Action of UIControlEventTouchDown type is added to the UISlider, which will trigger the $AppClick event when only clicking without sliding the UISlider. We prefer to trigger the $AppClick event only when the hand stops sliding the UISlider. Therefore, you need to modify - sensorsdata in the UIControl+SensorsData.m file_ The didmovetosuperview method also adds UIControlEventValueChanged type Action to the UISlider by default.

- (void)CountData_didMoveToSuperview {
    
    //Swap original methods before calling
    [self CountData_didMoveToSuperview];
    //Determine whether it is a special control
    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];
    }
}

The $AppClick event is always triggered during the sliding of the UISlider. Therefore, we also need to modify - countdata in the UIControl+CountData.m file_ Valuechanged action: Event: method to ensure that if it is a UISlider control, the $AppClick event is triggered only when the hand is raised.

-(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];
    }
    
}


After this processing, when we slide the UISlider, the $AppClick event will only be triggered when the hand is raised.

Scheme summary

Scheme 1 and scheme 2 actually use the target action mode in iOS. These two schemes have their own advantages and disadvantages.

  • For scheme 1: if multiple target actions are added to a control, the $AppClick event will be triggered multiple times.
  • For scheme 2: because the SDK adds a default trigger type Action to the control, if the developer uses the allTargets or allControlEvents property of UIControl class for logical judgment during development, some unexpected problems may be introduced. Therefore, when choosing a scheme, readers can determine the final implementation scheme according to their actual situation and needs.

Posted by lonewolf217 on Mon, 18 Oct 2021 12:46:30 -0700