stack overflow上的有人提出responder chain問(wèn)題UITableViewCell skipped in responder chain,并附上其git demo驗(yàn)證responder chain;我在閱讀代碼后,在此寫(xiě)下幾點(diǎn)體會(huì)
demo分析
代碼構(gòu)造的view層次結(jié)構(gòu)如下:

cell中button點(diǎn)擊操作代碼:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableCell *cell = [[TableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
ContentView *view = [[ContentView alloc] initWithFrame:cell.bounds];
cell.selectionStyle = UITableViewCellSelectionStyleBlue;
cell.backgroundColor = [UIColor greenColor];
view.backgroundColor = [UIColor clearColor];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 44)];
[button setTitle:@"Button" forState:UIControlStateNormal];
button.backgroundColor = [UIColor blueColor];
[button addTarget:nil action:@selector(customEventFired:) forControlEvents:(1 << 24)];
[button addTarget:self action:@selector(sendStuff:) forControlEvents:UIControlEventTouchUpInside];
[view addSubview:button];
[cell.contentView addSubview:view];
return cell;
}
- (void)sendStuff:(id)sender {
UIButton *btn = (UIButton *)sender;
[btn sendActionsForControlEvents:(1 << 24)];
}
點(diǎn)擊按鈕時(shí),會(huì)通過(guò)hitTest來(lái)查找touch事件是在哪個(gè)view上,之后觸發(fā)類中的方法sendStuff:,該方法中又會(huì)觸發(fā)controlEvent(1<<24),因?yàn)?code>[button addTarget:nil action:@selector(customEventFired:) forControlEvents:(1 << 24)];,target是nil,會(huì)根據(jù)responder chain來(lái)查找可以處理事件的responder;接下來(lái)先介紹hitTest過(guò)程。
ios事件分成三類:
- 觸摸事件(Touch event):觸摸事件會(huì)被分發(fā)給觸摸產(chǎn)生的view,查找這個(gè)view的過(guò)程就被成為 hitTest
- 運(yùn)動(dòng)事件(Motion event):會(huì)被分發(fā)給first responder處理
- 遠(yuǎn)程事件 (remote control event):同上
hitTest:withEvent:
touch event產(chǎn)生后,會(huì)被加入到app的事件隊(duì)列,按照先進(jìn)先出的原則,依次取出事件,系統(tǒng)先發(fā)給主窗口,主窗口按照view層次結(jié)構(gòu),去查找最小的發(fā)生觸摸事件的view;主要過(guò)程如下:
- 判斷view是否接收touch event,以下三種情況不接收:
- userInteractionEnabled為no
- hidden為YES
- alpha為0
- 觸摸點(diǎn)是否在view范圍中
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint btnPoint = [self convertPoint:point toView:_btn];
if ([_btn pointInsider:btnPoint withEvent:event])
{
return YES;
}
else
{
return [super pointInside:point withEvent:event];
}
}
- 依照上述過(guò)程遍歷子控件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint btnPoint = [_btn convertPoint:point fromView:self];
if ([_btn pointInside:btnPoint withEvent:event])
{
return _btn;
}
return [super hitTest:point withEvent:event];
}
- 如果沒(méi)有符合的子控件,則自身就是hitTest-view
在項(xiàng)目中,運(yùn)行結(jié)果調(diào)用,會(huì)發(fā)現(xiàn)

上述結(jié)果中沒(méi)有展示button的hitTest:withEvent:結(jié)果,其中{{0,0},{100,44}}是button的frame
注意clipsToBounds設(shè)置為NO,因?yàn)閟ubview可以超出superView,所以這個(gè)時(shí)候要重寫(xiě)
hitTest:withEvent:或者pointInside:withEvent:
responder chain
hitTest view會(huì)處理touch event,但是如果其不能處理;則會(huì)根據(jù)響應(yīng)鏈從firstResponder開(kāi)始往上傳遞,尋找可以處理的responder為止。
根據(jù)responder chain確認(rèn)event handler

first responder
被指派第一個(gè)接收事件,可以通過(guò)重寫(xiě)
- (BOOL)canBecomeFirstResponder {
NSLog(@"canBecomeFirstResponder");
return YES;
}
再調(diào)用[cell becomeFirstResponder],可以成為first responder;例如tableCell中重寫(xiě)canBecomeFirstResponder,并調(diào)用becomeFirstResponder,則在點(diǎn)擊button時(shí),響應(yīng)鏈并沒(méi)有調(diào)用contentView中的customEventFired:方法,而是調(diào)用tableCell中的customEventFired:方法。
參考
responder object
Event Delivery:The Responder Chain
iOS事件分發(fā)機(jī)制(一) hit-Testing
iOS事件分發(fā)機(jī)制(二)The Responder Chain