UIButton 分析

1,512 阅读6分钟

在做iOS开发的过程中,当我们遇到这样的需求:当点击某一区域时,响应某一事件。我们首先会想到button。button很形象,就像现实世界中的固话一样,输入电话号码时根据号码点击相应的按键。接下来我们从UIButton的API中去分析button实现。

UIButton继承链

首先看一下继承关系:

@interface UIButton : UIControl <NSCoding>
@interface UIControl : UIView
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>
@interface UIResponder : NSObject <UIResponderStandardEditActions>

上面是UIButton的继承链。在<objc/runtime.h>中有objc_class的定义:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

结构体中定义了objc_protocol_list,用于存储协议。我们在对一个类进行分析时,除了要看类自身的方法,以及通过继承拥有的方法,还需要去看协议中定义了那些方法。

UIButton

首先,我们来看一下UIButton自身包含哪些属性和方法。

@property(nullable, nonatomic,readonly,strong) UILabel     *titleLabel;
@property(nullable, nonatomic,readonly,strong) UIImageView *imageView;

UIButton是集成了UILable和UIImageView,用于信息展示。

@property(nonatomic,readonly) UIButtonType buttonType;

通过buttonType属性的设置,可以实现不同样式的button。

- (void)setTitle:(nullable NSString *)title forState:(UIControlState)state;
- (void)setTitleColor:(nullable UIColor *)color forState:(UIControlState)state;
- (void)setTitleShadowColor:(nullable UIColor *)color forState:(UIControlState)state;
- (void)setImage:(nullable UIImage *)image forState:(UIControlState)state;
- (void)setBackgroundImage:(nullable UIImage *)image forState:(UIControlState)state;
- (void)setAttributedTitle:(nullable NSAttributedString *)title forState:(UIControlState)state;

通过上述方法,设置UIButton的属性值。

@property(nonatomic) UIEdgeInsets contentEdgeInsets
@property(nonatomic) UIEdgeInsets titleEdgeInsets;
@property(nonatomic) UIEdgeInsets imageEdgeInsets;

调整内容视图的位置。

+ (instancetype)buttonWithType:(UIButtonType)buttonType;

初始化UIButton的方法。 以上是UIButton的一些方法和属性。接下来我们看一下UIControl。

UIControl

首先,我们来看一下都有哪些类继承自UIControl。

  1. UIButton
  2. UIDatePicker
  3. UIPageControl
  4. UISegmentedControl
  5. UISlider
  6. UIStepper
  7. UISwitch
  8. UITextField
  9. UITextView

差不多所有接收用户输入的控件都会继承UIControl。接下来,我们来看一下UIControl的API。

@property(nonatomic,getter=isEnabled) BOOL enabled;

默认值是YES,值为NO时,忽略touch事件。

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

对手指触摸做处理,产生相应的事件。

- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIContorlEvents)controlEvents;

添加事件监听、移除事件监听。

- (nullable NSArray<NSString *> *)actionsForTarget:(nullable id)target forControlEvent:(UIControlEvents)controlEvent;
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;

可以重写上述方法,改变方法实现。

UIView

@property(nonatomic,readonly,strong) CALayer  *layer;

为什么要把layer属性放在第一位?查看过CALayer文档,我们发现视图内容的显示是layer实现的。layer有contents属性,类型是id。我们可以通过赋值contents来显示内容。将UIView的API和CALayer的API作对比,可以发现两者是有很多共通之处的,比如:

CALayer

- (void)removeFromSuperlayer;
- (void)insertSublayer:(CALayer *)layer atIndex:(unsigned)idx;
- (void)replaceSublayer:(CALayer *)layer with:(CALayer *)layer2;
- (void)addSublayer:(CALayer *)layer;
- (void)insertSublayer:(CALayer *)layer below:(nullable CALayer *)sibling;
- (void)insertSublayer:(CALayer *)layer above:(nullable CALayer *)sibling;

- (nullable CALayer *)hitTest:(CGPoint)p;
- (BOOL)containsPoint:(CGPoint)p;

- (void)drawInContext:(CGContextRef)ctx;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)r;

UIView

- (void)removeFromSuperview;
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index;
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2;
- (void)addSubview:(UIView *)view;
- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview;
- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview;

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

- (CGPoint)convertPoint:(CGPoint)p fromLayer:(nullable CALayer *)l;
- (CGPoint)convertPoint:(CGPoint)p toLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r fromLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r toLayer:(nullable CALayer *)l;

- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;

- (void)drawRect:(CGRect)rect;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

方法完全类似。

CALayer

@property CGRect frame;
@property CGRect bounds;
@property CGPoint position;
@property CATransform3D transform;
... ... 

UIView

@property(nonatomic) CGRect            frame;
@property(nonatomic) CGRect            bounds;
@property(nonatomic) CGPoint           center;
@property(nonatomic) CGAffineTransform transform;
... ...

经过上述对比,我们可以认为UIView是对CALayer,以及CAAnimation的封装。CALLayer是视图展示层,UIView是视图展示层控制层。

感兴趣的朋友可以去做更深层次的对比。

对于UIView遵循的一系列协议,本篇就不一一详解了,感兴趣的朋友可以去翻阅一下。

接下来我们来看一下UIResponder。

UIResponder

我们首先来看几个属性和方法

- (nullable UIResponder*)nextResponder;

// touch
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

// press
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event;

// motion
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;

// remoteControl
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event;

- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender;
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender

上述代码中可以看出来,UIResponder主要是针对于事件的响应。

@interface UIResponder (UIResponderInputViewAdditions)

@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView;
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView;

@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController;
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController;

UIResponder还有其他的类别及协议。我们就先说到这里。

接下来我们尝试去看一下UIButton的事件响应过程。

UIButton响应链

试想一下,如果我们点击了屏幕上的某一个区域,并希望程序能响应一些操作,整个流程应该是怎么样的呢?

事件分发

首先,我们要知道我们点击的是哪个控件。如果我们站在应用程序的角度,我们不可能根据手指点击的位置直接获取到包含当前点的最小视图。我们需要从最大的视图(UIWindow)去层层向内递归,直至找到最小视图。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (self.alpha < 0.01 || self.userInteractionEnabled || self.hidden) {
        return nil;
    }
    
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    
    __block UIView *hitView = self;
    [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        hitView = [obj hitTest:point withEvent:event];
        if (hitView) {
            *stop = YES;
        }
    }];
    
    return hitView ? : self;
}

系统在寻找最小视图时使用的方法:

// recursively calls -pointInside:withEvent: point is in the receivers coordinate system
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

// default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

接下来我们实际测试一下

#import "UIView+ResponderChain.h"

#import <objc/runtime.h>

@implementation UIView (ResponderChain)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class currentClass = [self class];
        
        SEL originSel = @selector(hitTest:withEvent:);
        SEL newSel = @selector(customHitTest:withEvent:);
        
        Method originMethod = class_getInstanceMethod(currentClass, originSel);
        Method newMethod = class_getInstanceMethod(currentClass, newSel);
        
        BOOL addSuccess = class_addMethod(currentClass, originSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
        
        if (addSuccess) {
            class_replaceMethod(currentClass, newSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        } else {
            method_exchangeImplementations(originMethod, newMethod);
        }
    });
}

- (UIView *)customHitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"%@ %s", [self class], __FUNCTION__);
    return [self customHitTest:point withEvent:event];
}

@end

我们在视图控制器中添加view如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    AView *redView = [[AView alloc] initWithFrame:CGRectMake(30, 30, 200, 200)];
    redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:redView];
    
    BView *blueView = [[BView alloc] initWithFrame:CGRectMake(20, 20, 160, 160)];
    blueView.backgroundColor = [UIColor blueColor];
    [redView addSubview:blueView];
    
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.backgroundColor = [UIColor lightGrayColor];
    btn.frame = CGRectMake(20, 20, 120, 120);
    [btn addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
    [blueView addSubview:btn];
}

当我们点击button时, 我们来看一下输出结果:

现在我们已经找到了最小视图了,下面就是事件响应了。

事件响应

事件的响应依赖的是UIResponder,前文我们已经看过UIResponder的API了,对于事件的响应(以点击事件为例),UIResponder提供了touchesBegan、touchesMoved、touchesEnded、touchesCancelled。

我们在UIView的类别中通过方法替换替换touchBegan:

- (void)customTouchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@ --- %s", [self class], __FUNCTION__);
    [super touchesBegan:touches withEvent:event];
}

写到这里,整个事件的处理流程以及结束。事件的分发依赖的是图层,通过point定位到最小视图,然后通过UIResponder反向查询响应链。如果事件在传递到UIWindow依然无法处理,会继续上传到UIApplication,然后是delegate。appDelegate是完整响应链条的最后一环,如果事件在appDelegate出依然得不到处理,就会走消息派发。