Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
Writing in the front
Portal:
- IOS Full Burial Point – App exit and launch (1)
- IOS Full Buried Point – Page browsing event (2)
- IOS Full Buried Point – Control click event (3)
The previous chapters can be viewed above. This chapter mainly introduces the iOS full buried point sequence article (3) Control click event analysis
Target-action design mode
Before going into the details of how to do this, we need to understand the target-Action design pattern for click or drag events in UIKit. The target-action mode consists of two main parts.
Target
(object) : The object that receives the message.Action
(method) : Used to indicate the method that needs to be invoked
Target can be an object of any type. In iOS applications, however, it is usually a controller, and the object that triggers the event, like the object that receives the message (Target), can be any type of object. For example, the gesture recognizer UIGestureRecognizer can send a message to another object after recognizing a gesture.
When we add target-action to a control, how does the control find the Target and execute the corresponding Action?
– (void)sendAction:(SEL) Action to:(Nullable ID) Target forEvent:(Nullable UIEvent *)event;
When a user manipulates a control (such as a click), this method is first called and events are forwarded to the application’s UIApplication object.
There is also a similar instance method in the UIApplication class: – (BOOL)sendAction:(SEL)action to:(nullable id)target from:(nullable id)sender forEvent:(nullable UIEvent *)event;
If Target is not nil, the application makes that object call the corresponding method in response to the event; If Target is nil, the application searches the response chain for the object that defines the method and then executes the method.
Based on the target-Action design pattern, there are two ways to achieve full embedding of $AppClick events. We will introduce them one by one.
Plan a
describe
According to the target-Action design pattern, event-related information is sent through the control and UIApplication object before the Action is executed. Therefore, we can through the Method Swizzling exchange – sendAction: on the UIApplication class: the from: forEvent: Method, and then after the exchange Method of trigger $AppClick events, And collect related attributes according to target and sender, to achieve full buried $AppClick event.
Code implementation
Create a new UIApplication category
+ (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];
}
Copy the code
In general, for a control click event, we need to collect at least the following information (properties) :
- Control type (
$element_type
) - Control to display the text (
$element_content
) - Control page (
$screen_name
)
Get the control type
Let me show you the inheritance diagram for NSObject
As you can see from the above figure, controls inherit from UIView, so get To get the control type, you can declare UIView classes
New UIView category (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
Copy the code
UIView+TypeData.m
#import "UIView+TypeData.h" @implementation UIView (TypeData) - (NSString *)elementType { return NSStringFromClass([self class]); } @endCopy the code
Gets a buried 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]; Prams [@"$elementType "] = view.elementType; [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:prams]; return [self CountData_sendAction:action to:target from:sender forEvent:event]; }Copy the code
Gets the displayed text
To get the displayed text, we just need to call the corresponding method for the specific control. Let’s use UIButton as an example to illustrate the implementation steps. So first you declare a UIView category UIView+TextContentData, and then you add UIButton category UIButton category to UIView’s category UIView+TextContentData.
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
Copy the code
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
Copy the code
Gets the text buried 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]; 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]; }Copy the code
We are just using UIButton as an example. If you want to extend other controls, add the corresponding control category directly.
Gets the page to which the control belongs
How do you know that UIView belongs to that UIViewController, and that’s going to be UIResponder.
UIApplication, UIViewController, and UIView classes are all subclasses of UIResponder. In iOS applications, objects of UIApplication, UIViewController, and UIView classes are also responders. These responders form a responder chain.
A complete responder chain transfer rule (order) is as follows: UIView→UIViewController→UIWindow→UIApplication→UIApplicationDelegate
According to the response chain diagram, for any view, it can find the view controller it is in through the responder chain, that is, the page it belongs to, so as to obtain the information of the page it belongs to.
Note: Classes that implement the UIApplicationDelegate protocol in iOS applications (usually AppDelegate) will also participate in the responder chain if they inherit from UIResponder; If it does not inherit from UIResponder (such as NSObject), it does not participate in the responder chain.
UIView+TextContentData.h
@interface UIView (TextContentData)
@property (nonatomic,copy,readonly) NSString *elementContent;
@property (nonatomic,strong,readonly) UIViewController *myViewController;
@end
Copy the code
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
Copy the code
Get control page buried point implementation
+ (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; 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]; }Copy the code
More controls
You can obtain text information about UISwitch controls
Testing shows that the $AppClick event of UISwitch does not have the $element_content attribute. This problem can be explained by the fact that the UISwitch control itself does not display any text. To obtain the text information of the UISwitch control, we can set a simple rule: When the on attribute of the UISwitch control is YES, the text is “Checked”. The text is “unchecked” when the ON attribute of the UISwitch control is NO.
The solution declares the classification of UISwitch
@implementation UISwitch (TextContentData)
- (NSString *)elementContent {
return self.on ? @"checked":@"unchecked";
}
@end
Copy the code
Sliding the UISlider control repeatedly triggers the $AppClick event solution
Cause: When we were sliding the UISlider control, the system triggered UITouchPhaseBegan, UitouchPhase-Moved, UITouchPhaseMoved,… UITouchPhaseEnded, events, and each event will trigger a UIApplication – sendAction: : the from: forEvent: method performs, triggering $AppClick events. Prevents sliding UISlider from repeating a response, only after UITouchPhaseEnded begins a response
/ / to prevent sliding UISlider control the if (event. AllTouches. AnyObject. Phase = = UITouchPhaseEnded | | [sender isKindOfClass: [UISwitch class]]) { [[SensorsAnalyticsSDK sharedInstance] track:@"$appclick" properties:prams]; }Copy the code
Scheme 2
describe
When a view is added to the superview, the system automatically calls the -didMoveToSuperView method. So, we can swap UIView’s -didMoveToSuperView Method with Method Swizzling, and then add a set of UIControlEventTouchDown target-actions to the control in the swap Method, And trigger the $AppClick event in the Action, so as to achieve the $AppClick event buried, this is the implementation principle of scheme 2.
Code implementation
Create a UIControl classification
UIControl+CountData.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIControl (CountData)
@end
NS_ASSUME_NONNULL_END
Copy the code
UIControl+CountData.m
+ (void)load { [UIControl sensorsdata_swizzleMethod:@selector(didMoveToSuperview) withMethod:@selector(CountData_didMoveToSuperview)]; } - (void)CountData_didMoveToSuperview {// Swap the original method 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 UIView * view = (UIView *)sender; NSMutableDictionary *prams = [[NSMutableDictionary alloc]init]; Prams [@"$elementType "] = view.elementType; Prams [@"element_content"] = view.elementContent; prams[@"element_content"] = view.elementContent; UIViewController *vc = view.myViewController; prams[@"element_screen"] = NSStringFromClass(vc.class); [[SensorsAnalyticsSDK sharedInstance]track:@"appclick" properties:prams]; }}Copy the code
Note that the -didMoveToSuperView method is not actually implemented in UIControl; it inherits from its parent UIView. So what we’re actually swapping is the -didMoveToSuperView method in UIView. When a UIView object calls the -didMoveToSuperView method, it’s actually calling the -countData_didMoveToSuperView method implemented in UIControl+CountData.m. But UIView objects, or objects in UIView subclasses other than UIControl, when you do -countDatA_didMoveToSuperView, you don’t implement -countDatA_didMoveToSuperView, so, The program will crash when it can’t find a method.
Aiming at this problem, we need to modify NSObject + SASwizzler. M + sensorsdata_swizzleMethod in file: withMethod: class method, its is amended as: The method to be swapped is added to the current class before the method is swapped, and a new method pointer is obtained after the addition is successful.
+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL class_getInstanceMethod(self, originalSEL); if (! originalMethod) { return NO; } // Get Method alternateMethod = class_getInstanceMethod(self, alternateSEL); if (! alternateMethod) { return NO; } // get originalSel method implementation IMP originalIMP = method_getImplementation(originalMethod); Const char *originalMethodType = method_getTypeEncoding(originalMethod); // Add the originalSEL method to the class. Return NO if (class_addMethod(self, originalSEL, originalIMP, originalMethodType)) { OriginalMethod = class_getInstanceMethod(self, originalSEL); } // Get the alternateIMP method to implement IMP alternateIMP = method_getImplementation(alternateMethod); // Get the type of the alternateSEL method const char *alternateMethodType = method_getTypeEncoding(alternateMethod); // Add alternateSEL method to class, if it already exists, add failed, Return NO if (class_addMethod(self, alternateSEL, alternateIMP, alternateMethodType)) { Re-obtain the alternateSEL instance method alternateMethod = class_getInstanceMethod(self, alternateSEL); } // method_exchangeImplementations(originalMethod, alternateMethod); Return yes; }Copy the code
Support for more controls
Supports UISwitch, UISegmentedControl, and UIStepper controls
None of these controls respond to actions of type UIControlEventTouchDown, that is, the -sensorsDatA_touchDownAction: Event: method is not fired, and therefore, the $AppClick event is not fired. In fact, these controls add UIControlEventValueChanged type of Action.
+ (void)load { [UIControl sensorsdata_swizzleMethod:@selector(didMoveToSuperview) withMethod:@selector(CountData_didMoveToSuperview)]; } - (void)CountData_didMoveToSuperview {// Swap the original method before calling [self CountData_didMoveToSuperview]; / / determine whether if for some special controls ([self isKindOfClass: [UISwitch class]] | | [self isKindOfClass: [UISegmentedControl class]] | | [the 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 { $AppClick if (self.alltargets. Count > 2) {return YES; } // If the control itself is target and an Action of type other than UIControlEventTouchDown is added, the developer has added the control itself as target and added the Action // then return YES, Trigger the $AppClick event if((sell.allControlevents & UIControlEventAllEvents)! = UIControlEventTouchDown) { return YES; } // If the control is a Target, and two or more UIControlEventTouchDown actions are added, then return YES, Trigger $AppClick events if ([self actionsForTarget: self forControlEvent: defaultEvent]. Count > 2) {return YES; } return NO; }Copy the code
Supports UISlider controls
We added an Action of type UIControlEventTouchDown to the UISlider. This will cause the $AppClick event to be triggered even when we click on the UISlider without sliding it. We prefer that only the hand stops sliding the UISlider. Triggers the $AppClick event. Therefore, we need to modify the UIControl + SensorsData. M – sensorsdata_didMoveToSuperview methods of documents, the default also add UIControlEventValueChanged to UISlider type of Action.
- (void)CountData_didMoveToSuperview {// Swap the original method before calling [self CountData_didMoveToSuperview]; / / determine whether if for some special controls ([self isKindOfClass: [UISwitch class]] | | [self isKindOfClass: [UISegmentedControl class]] | | [the 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]; }}Copy the code
The $AppClick event is constantly triggered as you slide the UISlider. Therefore, we also need to modify the -countData_Valuechanged Action: Event: method in the UIControl+ countData. m file to ensure that if it is a UISlider control, the $AppClick event is only fired 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];
}
}
Copy the code
After this processing, when we slide the UISlider, the $AppClick event is only triggered when the hand is raised.
Project summary
In fact, both plan 1 and Plan 2 use the target-Action mode in iOS, and they have their own advantages and disadvantages.
- For plan one: If you add more than one control
Target-Action
Causes the $AppClick event to fire multiple times. - For plan two: because the SDK adds a default trigger type to the control
Action
Therefore, if the developer uses in the development processUIControl
Of the classallTargets
orallControlEvents
Attribute logical judgment may introduce some unexpected problems. Therefore, when choosing a scheme, readers can determine the final implementation scheme according to their own actual situation and needs.