从本质上讲,苹果设备响应事件的整个过程可以分为两个步骤:

步骤1:寻找目标。在iOS视图层次结构中找到触摸事件的最终接受者;
步骤2:事件响应。基于iOS响应者链(Responder Chain)处理触摸事件。

寻找目标

寻找目标是通过UIView的以下两个方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;//这个方法返回目标view
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; //这个方法判断触摸点是否在当前view范围内

寻找目标的过程也称为hit-Testing,整个过程可用下图表示:

iOS 事件的传递 ios事件传递与响应链_iOS 事件的传递

下面解释一下处理原理:

1、手指触摸屏幕,这个动作被包装成一个UIEvent对象发送给当前活跃的UIApplication (Active Application),Application将该Event对象插到任务队列的末尾等待处理(先进先出,先来的先处理);

2、UIApplication单例将事件发送给APP的主Window;

3、主Window调用视图层次结构上逐级使用hit-Testing确认最终的响应目标,这个目标也称为hitTesting view。

在没有做任何重载操作的前提下,系统默认的hit-Testing的处理机制如下:

1.当前view调用自身的pointInside: withEvent:方法判断触摸点是否在自己范围内;

 2. 若pointInside: withEvent:方法返回NO,则说明触摸点不在自己范围内,则当前view的hitTest: withEvent:方法返回nil,当前view上的所有subview都不做判断。

 3.若pointInside: withEvent:方法返回YES,则说明触摸点在自己的范围内。但无法判断是否在自己身上还是在subview的身上。此时,遍历所有的subviews,对每个subview调用hitTest方法。这里要注意,遍历的顺序**是从当前view的subviews数组的尾部开始遍历**。因此离用户最近的上层的subview会优先被调用hitTest方法。

 4.一旦hitTest方法返回非空的view,则被返回的view就是最终相应触摸事件的view,寻找hitTesting view的阶段到此结束,不再遍历。

 5.若当前view的所有subviews的hitTest方法都返回nil,则当前view的hitTest方法返回self作为最终的hitTesting view,处理结束。

以上就是第一阶段寻找响应view的机制。

需要注意的几点:

1、hitTest方法调用pointInside方法;

2、hit-Testing过程是从superView向subView逐级传递,也就是从层次树的根节点向叶子节点传递;

3、遇到以下设置时,view的pointInside将返回NO,hitTest方法返回nil:

  • view.isHidden=YES;
  • view.alpah<=0.01;
  • view.userInterfaceEnable=NO;
  • control.enable=NO;(UIControl的属性)

事件响应

通过hit-Testing机制找到了hitTesting View之后,下面就是进行事件响应了。这个hitTesting View就是触摸事件的响应者Responder。在iOS系统中,能够响应并处理事件的对象称之为Responder Object,而UIResponder是所有responder的最顶层基类。当hitTesting view做完自己该做的动作后,可以根据需要将消息传给下一级响应者。那下一级响应者会是什么呢?这取决于iOS中的响应者链Responder Chain,如下图所示:

iOS 事件的传递 ios事件传递与响应链_iOS_02

  • UIView的nextResponder属性,如果有管理此view的UIViewController对象,则为此UIViewController对象;否则nextResponder即为其superview。
  • UIViewController的nextResponder属性为其管理view的superview.
  • UIWindow的nextResponder属性为UIApplication对象。
  • UIApplication的nextResponder属性为nil。

更具体的:

1.如果hit-test view或first responder不处理此事件,则将事件传递给其nextResponder处理,若有UIViewController对象则传递给UIViewController,否则传递给其superView。

2.如果view的viewController也不处理事件,则viewController将事件传递给其管理view的superView。

3.视图层级结构的顶级为UIWindow对象,如果window仍不处理此事件,传递给UIApplication.

4.若UIApplication对象不处理此事件,则事件被丢弃。

实际用途:

响应者链在开发中可以解决那些问题呢?下面我就列举几个我在开发中遇到的问题:

  1. 通过view取出view所在的视图控制器ViewController(这段代码我是写在view的category里的):
  2. iOS 事件的传递 ios事件传递与响应链_iOS_03

  3. 当然了,你也可以通过响应者链取得其它类,不限于ViewController。
  4. 超出父视图的按钮响应事件。一般情况下我们的view是不会超出父视图的,但是在有些特殊的情况下,为了封装与坐标计算方便,子视图会超出。比如高德地图的“气泡”,在我们自定义气泡,你会发现上面添加的按钮是无法点击的,因为高德地图的气泡是跟“大头针”相关联的,所以弹出的气泡添加在了大头针上,大头针的大小是固定的,而且比起泡小得多。这总情况下就要重写气泡CustomAnnotationView的hitTest: withEvent:方法了:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    if (view == nil) {
        point = [self.calloutView.navBtn convertPoint:point fromView:self];
        if ([self.calloutView.navBtn.bounds pointInside:point withEvent:event])
        {
            view = self.calloutView.navBtn;
        }
    }
    return view;
}

这里的self.calloutView.navBtn 就是你需要点击的按钮。

在开发项目时,也遇到了类似地图大头针这样的需求,如下图:

iOS 事件的传递 ios事件传递与响应链_iOS_04

图上的标签位置是后台返回的,坐标以小红点为参考系,标签的文字长度是随文字变化的,点击标签进入相应的详情页。像这样的需求,首先我们可以看到这个标签是一个整体在许多地方都有用到,所以需要对它进行封装。具体代码如下:

#import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, YFTagsViewType) {
    YFTagsViewTypeLeft = 0, //左侧
    YFTagsViewTypeRight = 1 //右侧
};

@interface YFTagsView : UIView

@property (nonatomic, copy) NSString *title;    //标题
@property (nonatomic, strong) UIImage *ico;     //图标
@property (nonatomic, assign) BOOL isImage;     //是否显示图标
@property (nonatomic, assign) CGFloat minWidth; //最小宽度
@property (nonatomic, assign) CGFloat maxWidth; //最大宽度
@property (nonatomic, assign) YFTagsViewType type;
/*
 * 创建方法:
 * frame位置大小(YFTagsViewTypeLeft:右侧固定,宽度随变化。YFTagsViewTypeRight:相反)
 * type:类型
 */
- (instancetype)initWithFrame:(CGRect)frame Type:(YFTagsViewType)type;

@end
#import "YFTagsView.h"
#import "UIView+Animation.h"

#define SNN ([UIScreen mainScreen].bounds.size.width)/(375)
#define kZoom6pt(pt) ((pt)*(SNN))

#define KArrorWeight kZoom6pt(15)
#define space 0.70

@implementation YFTagsView {
    UILabel *titleLabel;
    UIImageView *imgView;
    UIView *view;
    NSArray *layers;
}

- (void)drawRect:(CGRect)rect {
    BOOL isbool = YES;
    if (_type == YFTagsViewTypeLeft) {
        isbool = NO;
    }

    CGRect rrect = self.bounds;
    CGFloat radius = kZoom6pt(6),
    arrorRadius = kZoom6pt(3),
    arrorWeight = isbool?CGRectGetHeight(rrect)*space:-CGRectGetHeight(rrect)*space,

    minx = isbool?CGRectGetMinX(rrect) + kZoom6pt(10):CGRectGetMaxX(rrect) - kZoom6pt(10),
    maxx = isbool?CGRectGetMaxX(rrect):CGRectGetMinX(rrect),

    miny = CGRectGetMinY(rrect),
    midy = CGRectGetMidY(rrect),
    maxy = CGRectGetMaxY(rrect);

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:CGPointMake(minx, midy)];

    [path addCurveToPoint:CGPointMake(minx + arrorWeight, miny)
            controlPoint1:CGPointMake(minx, midy - arrorRadius)
            controlPoint2:CGPointMake(minx + arrorWeight - (isbool?arrorRadius:-arrorRadius), miny)];

    [path addLineToPoint:CGPointMake(maxx - (isbool?radius:-radius), miny)];
    [path addQuadCurveToPoint:CGPointMake(maxx, miny + radius) controlPoint:CGPointMake(maxx, miny)];

    [path addLineToPoint:CGPointMake(maxx, maxy - radius)];
    [path addQuadCurveToPoint:CGPointMake(maxx - (isbool?radius:-radius), maxy) controlPoint:CGPointMake(maxx, maxy)];

    [path addLineToPoint:CGPointMake(minx + arrorWeight, maxy)];
    [path addCurveToPoint:CGPointMake(minx, midy)
            controlPoint1:CGPointMake(minx + arrorWeight - (isbool?arrorRadius:-arrorRadius), maxy)
            controlPoint2:CGPointMake(minx, midy + arrorRadius)];

    [path closePath];
    UIColor *fillColor = [UIColor colorWithWhite:0.0 alpha:0.65];
    [fillColor set];
    [path fill];
}

- (instancetype)initWithFrame:(CGRect)frame Type:(YFTagsViewType)type {
    self = [super initWithFrame:frame];
    if (self) {
        _type = type;
        _minWidth = CGRectGetHeight(self.frame)*space + kZoom6pt(32);
        _maxWidth = [UIScreen mainScreen].bounds.size.width;
        self.backgroundColor = [UIColor clearColor];
        [self initSubViews];
    }
    return self;
}

- (void)initSubViews {
    imgView = [[UIImageView alloc] init];
    imgView.contentMode = UIViewContentModeScaleAspectFill;
    imgView.clipsToBounds = YES;
    imgView.image = [UIImage imageNamed:@"shoping"];;
    imgView.hidden = !_isImage;
    [self addSubview:imgView];

    titleLabel = [[UILabel alloc] init];
    titleLabel.font = [UIFont systemFontOfSize:kZoom6pt(12)];
    titleLabel.textColor = [UIColor whiteColor];
    titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
    titleLabel.textAlignment = _type == YFTagsViewTypeLeft?NSTextAlignmentLeft:NSTextAlignmentRight;
    [self addSubview:titleLabel];

    view = [[UIView alloc] init];
    view.backgroundColor = [UIColor colorWithWhite:1 alpha:0.75];
    view.layer.cornerRadius = kZoom6pt(5);
    view.hidden = _isImage;
    [self addSubview:view];

    CALayer *layer1 = [CALayer layer];
    layer1.frame = CGRectMake(0, 0, kZoom6pt(10), kZoom6pt(10));
    layer1.cornerRadius = kZoom6pt(5);
    layer1.backgroundColor = [UIColor colorWithWhite:1 alpha:0.75].CGColor;
    [view.layer addSublayer:layer1];

    CALayer *layer2 = [CALayer layer];
    layer2.frame = CGRectMake(0, 0, kZoom6pt(10), kZoom6pt(10));
    layer2.cornerRadius = kZoom6pt(5);
    layer2.backgroundColor = [UIColor colorWithWhite:1 alpha:0.75].CGColor;
    [view.layer addSublayer:layer2];
    layers = @[layer1, layer2];

    UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(kZoom6pt(2.5), kZoom6pt(2.5), kZoom6pt(5), kZoom6pt(5))];
    view1.backgroundColor = [UIColor colorWithRed:255/255.0 green:63/255.0 blue:139/255.0 alpha:255/255.0];
    view1.layer.cornerRadius = kZoom6pt(2.5);
    [view addSubview:view1];

    [self loadFrame];
}

- (void)loadFrame {
    titleLabel.textAlignment = _type == YFTagsViewTypeLeft?NSTextAlignmentLeft:NSTextAlignmentRight;
    CGFloat width = 0;
    NSString *str = titleLabel.text;
    //限制最多7个字
    for (int i = 7; i > 0; i--) {
        if (i <= str.length) {
            NSString *subStr = [str substringToIndex:i];
            width = [NSString widthWithString:subStr
                                         font:[UIFont systemFontOfSize:kZoom6pt(12)]
                          constrainedToHeight:kZoom6pt(18)] + CGRectGetHeight(self.frame)*space;
            width += _isImage?kZoom6pt(40):kZoom6pt(16);

            //宽度在最大与最小之间跳出循环
            if (width >= _minWidth && width <= _maxWidth) {
                break;
            }
        }
    }

    if (width <  _minWidth) {
        width = _minWidth;
    }


    CGRect frame = self.frame;
    CGFloat x = (width - frame.size.width);
    frame.size.width = width;

    if (_type == YFTagsViewTypeRight) {
        view.frame = CGRectMake(0, (frame.size.height - kZoom6pt(10))/2, kZoom6pt(10), kZoom6pt(10));
        imgView.frame = CGRectMake(frame.size.width - kZoom6pt(24), kZoom6pt(3), kZoom6pt(18), kZoom6pt(18));
        if (_isImage) {
            titleLabel.frame = CGRectMake(kZoom6pt(10) + CGRectGetHeight(self.frame)*space, kZoom6pt(3), frame.size.width - kZoom6pt(40) - CGRectGetHeight(self.frame)*space, kZoom6pt(18));
        } else {
            titleLabel.frame = CGRectMake(kZoom6pt(10) + CGRectGetHeight(self.frame)*space, kZoom6pt(3), frame.size.width - kZoom6pt(16) - CGRectGetHeight(self.frame)*space, kZoom6pt(18));
        }
        self.frame = frame;
    } else if(_type == YFTagsViewTypeLeft){
        frame.origin.x -= x;
        view.frame = CGRectMake(frame.size.width - kZoom6pt(10), (frame.size.height - kZoom6pt(10))/2, kZoom6pt(10), kZoom6pt(10));
        imgView.frame = CGRectMake(kZoom6pt(6), kZoom6pt(3), kZoom6pt(18), kZoom6pt(18));
        if (_isImage) {
            titleLabel.frame = CGRectMake(kZoom6pt(30), kZoom6pt(3), frame.size.width - kZoom6pt(40) - CGRectGetHeight(self.frame)*space, kZoom6pt(18));
        } else {
            titleLabel.frame = CGRectMake(kZoom6pt(6), kZoom6pt(3), frame.size.width - kZoom6pt(16) - CGRectGetHeight(self.frame)*space, kZoom6pt(18));
        }
        self.frame = frame;
    }
    [self setNeedsDisplay];

    //动画
    [view scaleStatus:YES layers:layers];
}


#pragma mark - setter and getter

- (void)setTitle:(NSString *)title {
    if ([title isEqual:[NSNull null]]) {
        title = @"";
    }
    NSArray *nameAry = [title componentsSeparatedByString:@"】"];
    NSString *name = [nameAry lastObject];
    _title = name?:@"";
    titleLabel.text = _title;
    [self loadFrame];
}

- (void)setIco:(UIImage *)ico {
    _ico = ico;
    imgView.image = _ico;
}

- (void)setIsImage:(BOOL)isImage {
    _isImage = isImage;
    imgView.hidden = !_isImage;
    [self loadFrame];
}

- (void)setMinWidth:(CGFloat)minWidth {
    if (minWidth > CGRectGetHeight(self.frame)*space + kZoom6pt(40)) {
        _minWidth = minWidth;
    } else {
        _minWidth = CGRectGetHeight(self.frame)*space + kZoom6pt(40);
    }

    if (minWidth > [UIScreen mainScreen].bounds.size.width) {
        _minWidth = [UIScreen mainScreen].bounds.size.width;
    }
}

- (void)setMaxWidth:(CGFloat)maxWidth {
    if (maxWidth < [UIScreen mainScreen].bounds.size.width) {
        _maxWidth = maxWidth;
    } else {
        _maxWidth = [UIScreen mainScreen].bounds.size.width;
    }

    if (maxWidth < CGRectGetHeight(self.frame)*space + kZoom6pt(40)) {
        _maxWidth = CGRectGetHeight(self.frame)*space + kZoom6pt(40);
    }
}

@end
#import <UIKit/UIKit.h>
#import "YFTagsView.h"

@interface YFTagButton : UIControl

// 创建方法
- (instancetype)initWithType:(YFTagsViewType)type;
+ (instancetype)buttonWithType:(YFTagsViewType)type;

/*
 @brief 赋值
 @param title:   标题
 @param price:   价格
 @param origin:  坐标(动画小圆点的中心)
 @param isImg:   是否显示购物车图标及价格标签
 @param type:    类型
 */
- (void)setTitle:(NSString *)title price:(NSString *)price origin:(CGPoint)origin isImg:(BOOL)isImg type:(YFTagsViewType)type;

@end
#import "YFTagButton.h"
#import "GlobalTool.h"

#define scax 1

@implementation YFTagButton {
    UIImageView *imgView;
    UILabel *priceLabel;
    YFTagsView *tagView;
    CGPoint _origin;
    NSString *_title;
    NSString *_price;
    BOOL _isImg;
    YFTagsViewType _type;
}

+ (instancetype)buttonWithType:(YFTagsViewType)type {
    YFTagButton *btn = [[YFTagButton alloc] initWithType:type];
    return btn;
}

- (instancetype)initWithType:(YFTagsViewType)type {
    self = [super init];
    if (self) {
        _type = type;
        [self setUI];
    }
    return self;
}

- (instancetype)init {
    if (self = [super init]) {
        [self setUI];
    }
    return self;
}

- (void)setUI {
    imgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"price_ tag"]];
    imgView.frame = CGRectMake(kZoom6pt(-16), kZoom6pt(-5), kZoom6pt(32), kZoom6pt(43));
    [self addSubview:imgView];
    priceLabel = [[UILabel alloc] init];
    priceLabel.textColor = [UIColor whiteColor];
    priceLabel.textAlignment = NSTextAlignmentCenter;
    priceLabel.font = [UIFont systemFontOfSize:kZoom6pt(9)];
    priceLabel.frame = CGRectMake(0, imgView.height - kZoom6pt(20), kZoom6pt(32), kZoom6pt(20));
    [imgView addSubview:priceLabel];
    tagView = [[YFTagsView alloc] initWithFrame:CGRectMake(_type == YFTagsViewTypeRight?kZoom6pt(-5):kZoom6pt(-20), -kZoom6pt(12), kZoom6pt(25), kZoom6pt(24)) Type:_type];
    tagView.userInteractionEnabled = NO;
    [self addSubview:tagView];
}

- (void)setTitle:(NSString *)title price:(NSString *)price origin:(CGPoint)origin isImg:(BOOL)isImg type:(YFTagsViewType)type{
    _type = type;
    _origin = origin;
    _title = title?:@"";
    _price = price?:@"";
    _isImg = isImg;
    if (self.superview == nil) {
        return;
    }
    tagView.frame = CGRectMake(_type == YFTagsViewTypeRight?kZoom6pt(-5):kZoom6pt(-20), -kZoom6pt(12), kZoom6pt(25), kZoom6pt(24));
    CGPoint point = _type == YFTagsViewTypeLeft?CGPointMake(kZoom6pt(168),kZoom6pt(110)):CGPointMake(kZoom6pt(204),kZoom6pt(237));
    if (origin.x||origin.y) {
        CGFloat h = (scax*self.superview.width - self.superview.height)/2;
        point = CGPointMake(self.superview.width*origin.x, origin.y*self.superview.width*scax - h);
    }
    self.origin = point;
    CGFloat maxWidth = _type == YFTagsViewTypeLeft?point.x + kZoom6pt(5):self.superview.width - point.x + kZoom6pt(5);
    tagView.type = type;
    tagView.maxWidth = maxWidth;
    tagView.isImage = isImg;
    tagView.title = title?:@"";
    priceLabel.text = price?:@"";
    imgView.hidden = !isImg;
    self.userInteractionEnabled = isImg;
}

- (void)didMoveToSuperview {
    [super didMoveToSuperview];
    [self setTitle:_title price:_price origin:_origin isImg:_isImg type:_type];
}

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint tagPoint =  [touch locationInView:tagView];
    CGPoint imgPoint =  [touch locationInView:imgView];
    if ([tagView pointInside:tagPoint withEvent:event]||[imgView pointInside:imgPoint withEvent:event]) {
        [self sendActionsForControlEvents:UIControlEventTouchUpInside];
    }
    return NO;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    if (view == nil&&self.userInteractionEnabled) {
        CGPoint tagPoint = [tagView convertPoint:point fromView:self];
        CGPoint imgPoint = [imgView convertPoint:point fromView:self];
        if ([tagView pointInside:tagPoint withEvent:event]||[imgView pointInside:imgPoint withEvent:event]) {
            view = self;
        }
    }
    return view;
}

@end