In the development process, it is often encountered that the size of the button given by design is too small. This UIButton can be very difficult to click in use, greatly reducing the user experience

Solution 1: Rewrite UIButton’s – (BOOL)pointInside (CGPoint) Point withEvent (UIEvent*)event method

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {CGRect bounds = self.bounds; CGFloat widthDelta = MAX(44.0-bounds.size. width, 0); CGFloat widthDelta = MAX(44.0-bounds.size. width, 0); CGFloat heightDelta = MAX(44.0 - bound.size. Height, 0); CGFloat heightDelta = MAX(44.0 - bound.size. // Expand bounds bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta); // If the point is in the new bounds, return YES return CGRectContainsPoint(bounds, point); }Copy the code

The default value is:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return CGRectContainsPoint(self.bounds, point); 
}
Copy the code

In fact, the bounds of the response area were changed while judging. The CGRectInset(view, 10, 20) method indicates that the RECT size is modified

– (UIView) hitTest:(CGPoint) point withEvent:(UIEvent) event with the newly set Rect to click on the range.

#import "UIButton+EnlargeTouchArea.h"
#import <objc/runtime.h>

@implementation UIButton (EnlargeTouchArea)

static char topNameKey;
static char rightNameKey;
static char bottomNameKey;
static char leftNameKey;

- (void)setEnlargeEdgeWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left
{
    objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void)setTouchAreaToSize:(CGSize)size
{
    CGFloat top = 0, right = 0, bottom = 0, left = 0;
    
    if (size.width > self.frame.size.width) {
        left = right = (size.width - self.frame.size.width) / 2;
    }
    
    if (size.height > self.frame.size.height) {
        top = bottom = (size.height - self.frame.size.height) / 2;
    }
    
    [self setEnlargeEdgeWithTop:top right:right bottom:bottom left:left];
}

- (CGRect)enlargedRect
{
    NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);
    NSNumber *rightEdge = objc_getAssociatedObject(self, &rightNameKey);
    NSNumber *bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
    NSNumber *leftEdge = objc_getAssociatedObject(self, &leftNameKey);
    if (topEdge && rightEdge && bottomEdge && leftEdge)
    {
        return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
                          self.bounds.origin.y - topEdge.floatValue,
                          self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
                          self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
    }
    else
    {
        return self.bounds;
    }
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGRect rect = [self enlargedRect];
    if (CGRectEqualToRect(rect, self.bounds) || self.hidden)
    {
        return [super hitTest:point withEvent:event];
    }
    return CGRectContainsPoint(rect, point) ? self : nil;
}

@end

Copy the code

Solution 3: Use runtime swizzle to swap IMP

+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSError *error = nil; [self hg_swizzleMethod:@selector(pointInside:withEvent:) withMethod:@selector(hitTest_pointInside:withEvent:) error:&error]; NSAssert(! error, @"UIView+HitTest.h swizzling failed: error = %@", error); }); } - (BOOL)hitTest_pointInside:(CGPoint)point withEvent:(UIEvent *)event { if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) { return [self hitTest_pointInside:point withEvent:event]; } CGRect relativeFrame = self.bounds; CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets); return CGRectContainsPoint(hitFrame, point); }Copy the code

Categories were created to make it easier for developers to extend a class, not to let you change a class.

Summary of Technical Points

An associative object, that is, a binding object, can be bound to anything

// objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC); // self associated class, //key: To ensure global uniqueness, key and associated object are one-to-one correspondence. Must be globally unique //value: the object to be associated with the class. //policy: indicates the association policy. There are five association strategies. //OBJC_ASSOCIATION_ASSIGN is equivalent to @property(assign). //OBJC_ASSOCIATION_RETAIN_NONATOMIC equals @property(strong, //nonatomic). //OBJC_ASSOCIATION_COPY_NONATOMIC is equivalent to @property(copy, nonatomic). //OBJC_ASSOCIATION_RETAIN is equivalent to @property(strong,atomic). //OBJC_ASSOCIATION_COPY is equivalent to @property(copy, atomic). NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);Copy the code
Objc_setAssociatedObject is equivalent to setValue:forKey associates the value object objc_getAssociatedObject reads the object objc_AssociationPolicy Property is the property that sets the value in the object, namely assgin, (retain,nonatomic)... Wait for the objc_removeAssociatedObjects function to remove an associated object, or use the objc_setAssociatedObject function to set the associated object specified by key to nil.Copy the code

Method Swizzling

For classes that already exist, we usually use the +load method, or can’t get the class file, so we create a class and also load swizzling with its +load method

  • Swizzling should always be performed in +load
  • Swizzling should always be performed in dispatch_once
  • Do not call [super Load] when Swizzling is executed in +load. If [super Load] is called more than once, it may appear that “Swizzle is invalid”.

Exchange instance method

In the class as a class

Void class_swizzleInstanceMethod(Class Class, SEL originalSEL, SEL replacementSEL) {//class_getInstanceMethod(), If the subclass does not implement the corresponding method, the method of the parent class is returned. Method originMethod = class_getInstanceMethod(class, originalSEL); Method replaceMethod = class_getInstanceMethod(class, replacementSEL); //class_addMethod() determines if originalSEL is implemented ina subclass. If we simply inherit the methods of the parent class, we will call method_exchangeImplementations, then that will swap the methods in the parent class with the current implementation method. // When class_addMethod() returns YES, it returns an unrecognized selector. // When class_addMethod() returns YES, If the subclass does not implement this method (judged by SEL), class_addMethod adds a method named originalSEL and implemented as replaceMethod. Replace the replacementSEL implementation with the originMethod implementation. // When class_addMethod() returns NO, there is an implementation method in the subclass. In this case, we will call method_exchangeImplementations exchanging the implementations of the two methods. // Note: If you implement this method in a subclass, even if you simply call super, you are overwriting the parent method, so class_addMethod() returns NO. BaseClass if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod))) { class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod)); }else { method_exchangeImplementations(originMethod, replaceMethod); }}Copy the code

The problem here is that the subclass does not implement the parent class method when inheriting: Void test (); void test (); void test (); void test (); If subclass B does not have an implementation of the test method, the test method of base class A will be replaced with testRelease. If subclass B only uses the test method, there will be no problem. But when we use the test method of base class A, because test points to the IMP of the original testRelease, and base class A does not have this implementation, because we are written in subclass B. So you have an unrecognized selector

Exchange class method

Since class methods are stored in metaclass and exist as instance methods, the essence is to exchange instance methods of metaclass above. On the basis of exchanging instance methods, pass CLS as metaclass. The obtained metaclass can be objc_getMetaClass(“ClassName”) or object_getClass ([NSObject class])

Event responder chain

As shown in the figure, no further elaboration

Two important methods

- (nullable UIView*)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; Call method A - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; It's called method BCopy the code

When you override these two methods on the View, when you click on the screen, the first response is method A;

  • If in method A we do not call the parent class ([super hitTest:point withEvent:event];) Method A returns the view as the view that responds to the event. (Of course it returns nil, because this view is not responding)

  • If in method A we call the parent method ([super hitTest:point withEvent:event];) That’s when the system calls method B; The return value of this method is used to determine whether the current view can respond to the message

  • If method B returns no, it doesn’t have to iterate through its subviews. The view returned by method A is the view that can respond to events.

  • If method B returns YES, go through its subviews. (As we described above, the appropriate view is found and returned. If not, the view returned by method A is used to respond to the event.)

conclusion

Return a view in response to the event (it is best to call the parent method within this method if you do not want to affect the system’s event chain)

- (nullable UIView*)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event{
    return [super hitTest:point withEvent:event]; 
}
Copy the code

The value returned can be used to determine whether to continue traversing the subview (based on whether the touched point is within the frame range of the view)

- (BOOL)pointInside:(CGPoint)point withEvent:(nullableUIEvent *)event;Copy the code