iOS中三種事件類型
- 觸屏事件(Touch Event)
- 運動事件(Motion Event)
- 遠端控制事件(Remote-Control-Event)
響應者對象(Responder Object)
響應者對象指的是有響應和處理上述3種事件能力的對象。響應者鏈就是由一系列響應者對象構成一個層次結構。
UIResponder是所有響應對象的基類,在UIResponder類中定義了處理上述各種事件的接口。我們熟悉的UIApplication、UIWindow、UIViewController、UIView都直接或間接繼承自UIResponder,所以它們都是可以構成響應者鏈的響應者對象。
響應鏈的傳遞

盜圖真香
這張圖清晰的解釋了響應鏈的傳遞過程:
- 當發(fā)生觸屏事件后,系統(tǒng)會將事件加到
UIApplication管理的一個任務隊列中,并將事件分發(fā)下去。 - 通常先發(fā)送給
keyWindow,UIWindow繼續(xù)在其視圖層次結構中找到一個最合適的視圖來處理事件。 -
UIWindow會在它視圖上調用hitTest:withEvent:方法,hitTest:withEvent又會調用自身的pointInside:方法,若返回YES,說明點擊區(qū)域在UIWindow范圍內,然后UIWindow遍歷它子視圖(后添加的子視圖先遍歷)調用hitTest:WithEvent:方法。 - 上圖
UIWindow遍歷子視圖MainView,MainView調用自身hitTest:withEvent方法,且pointInside:方法返回YES,繼續(xù)遍歷子視圖ViewC。 -
ViewC調用自身hitTest:withEvent:方法,結果發(fā)現(xiàn)pointInside:方法返回NO,hitTest:方法返回nil;輪到ViewB。 -
ViewB調用自身hitTest:withEvent:方法,結果發(fā)現(xiàn)pointInside:方法返回YES,繼續(xù)遍歷子視圖ViewB.2 ViewB.1。 - 遍歷到
ViewB.1無子視圖可以遍歷,遍歷終止,hitTest:方法中返回自身即ViewB.1。 - 到此響應鏈結束,
ViewB.1響應了事件。
模擬系統(tǒng)的調用過程
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (self.userInteractionEnabled
&& !self.hidden
&& [self pointInside:point withEvent:event]) {
// 使用reverseObjectEnumerator進行倒序遍歷
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
// 將像素point從view中轉換到當前視圖中,返回在當前視圖中的像素值
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *responseView = [subview hitTest:convertedPoint withEvent:event];
if (responseView) {
return responseView;
}
}
//無子視圖返回自身
return self;
}
return nil;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
//判斷點擊位置是否在視圖范圍內
if (CGRectContainsPoint(self.bounds, point)) {
return YES;
}
return NO;
}
解決實際問題
1、響應超出父視圖外區(qū)域的事件
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
if (CGRectContainsPoint(self.bounds, point)) {
return YES;
}
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
// 將像素point從view中轉換到當前視圖中,返回在當前視圖中的像素值
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
BOOL inside = [subview pointInside:convertedPoint withEvent:event];
if (inside) {
return YES;
}
}
return NO;
}
2、面試題superView上添加viewA,viewA上添加viewB,viewB上添加viewC,且B、C都不在各自視圖內。此時重寫viewB的pointInside:方法并返回YES,點擊A和點擊B分別響應哪個視圖的事件。
[viewSuper addSubview:viewA];
[viewA addSubview:viewB];
[viewB addSubview:viewC];
UITapGestureRecognizer *tapA = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapA:)];
[viewA addGestureRecognizer:tapA];
UITapGestureRecognizer *tapB = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapB:)];
[viewB addGestureRecognizer:tapB];
UITapGestureRecognizer *tapC = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapC:)];
[viewC addGestureRecognizer:tapC];
UITapGestureRecognizer *tapS = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapS:)];
[viewSuper addGestureRecognizer:tapS];
- (void)tapA:(UITapGestureRecognizer *)tap {
NSLog(@"tapA");
}
- (void)tapB:(UITapGestureRecognizer *)tap {
NSLog(@"tapB");
}
- (void)tapC:(UITapGestureRecognizer *)tap {
NSLog(@"tapC");
}
- (void)tapS:(UITapGestureRecognizer *)tap {
NSLog(@"tapSuper");
}

1616056611363.jpg
答:點擊A,響應事件B,打印tapB
解:
- 先viewSuper調用
hitTest:方法并且pointInside:返回YES; - 遍歷子視圖ViewA,ViewA調用
hitTest:并且點在范圍內pointInside:返回YES; - 遍歷子視圖ViewB,ViewB調用
hitTest:雖然點不在范圍內,但pointInside:返回YES; - 接著遍歷ViewC,點擊的點不在ViewC范圍內
pointInside:返回NO; - ViewB的
hitTest:返回自身;所以響應了事件B;
答:點擊B,響應事件Super,打印tapSuper
解: - 先viewSuper調用
hitTest:方法并且pointInside:返回YES; - 遍歷子視圖ViewA,因為ViewB上的點不在ViewA范圍內,所以
pointInside:返回NO; - viewSuper的
hitTest:返回自身;所以響應了事件Super;