前两天面试遇到响应者链问题。有个需求按钮的frame超出其父视图,需要点击按钮任意区域都响应。如果UIView的子视图超出其frame,其子视图是收不到点击事件消息的。 参考资料:[iOS 响应者链](http://www.jianshu.com/p/09ea3fff3ffd) 整个过程通过`hitTest(_:with:)`和`point(inside:with:)`找到第一响应者,并生成响应者链。 ## 响应者链的产生 `AppDelegate`是点击事件的第一个接受者,它用一个队列保存由系统将手指触摸(Touch)操作打包的UIEvent对象,然后由`UIApplication.shared`从队列里取出事件,将事件传递给UIWindow对象处理。(iOS上UIApplication的UIWindow对象只有一个) UIWindow对象使用`hitTest(_:with:)`查找第一响应者。首先它会调用`pointInside:withEvent:`判断点击事件是否在当前视图里(我认为在window上不需要判断,点击事件一定是在window视图里),如果是在当前视图那就给其所有子视图发送`hitTest(_:with:)`消息,顺序是从最后加入的子视图开始。 window的子视图收到`hitTest(_:with:)`消息的时候调用`pointInside:withEvent:`判断事件是否在它的范围里,如果不是就停止响应,如果是那就和window一样把事件告诉其子视图,并且将该视图放入响应者栈。 如所有子视图都返回非,则`hitTest(_:with:)`方法返回自身(self),说明第一响应者找到了,也将其放入响应者栈,整个响应链也就生成了。 ## 事件的响应 当找到第一响应者后就开始响应事件,如果第一响应者不处理,就从响应链取下一个,直到有响应者处理了事件。 如果传递到最后的`AppDelegate`也没处理,那就丢弃该事件。 ## 解决方案 做法是自定义按钮的父视图类,然后在这个类里面重写`pointInside:withEvent:`方法,如果点击的区域超出其范围就利用按钮的frame和点击的坐标判断是否点击在按钮上。 代码: ```swift //为UIView写的判断点击的坐标是否在显示效果上的View上面(只能处理矩形) extension UIView { func isInside(_ point: CGPoint) -> Bool { let x = point.x let y = point.y let selfx = self.frame.origin.x let selfy = self.frame.origin.y let selfw = self.frame.size.width let selfh = self.frame.size.height let gapx = x - selfx let gapy = y - selfy if gapx >= 0 && gapy >= 0 && gapx <= selfw && gapy <= selfh{ return true } return false } } //在自定义的TestView类里重写该方法,为TestView扩充可响应区域 override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { var flag = super.point(inside: point, with: event) if flag { return flag } self.subviews.forEach { (v) in if v.isInside(point) { flag = true } } return flag } ``` 通过上面的方法就可以实现点击按钮超出父视图部分也能响应了
没有评论