响应者链bash
在 iOS 中事件响应的处理对象都是 UIResponder 对象,它的子类包括 UIView, UIViewController, UIApplication 等。当一个触发事件被 App 检测到时它会找一个合适的 UIResponder 对象作为 firstResponder 第一响应者,事件要么被它处理,要传递给另一个响应者(或者最后不处理)。而一个事件被一个响应者传递给其它响应者的过程就是 响应者链。app
app 的响应者链以下所示:ide
响应者链条的传递规则以下:ui
AViewController.present(BViewController)
, 那么 BViewController 的下一个响应者就是 AViewController第一个响应者的的检测this
当一个触控事件触发时,UIkit 使用 hitTest(_:with:) 返回触发对象是哪个。spa
好比若是点击了一个 view 对象,那么这个 view 对象就是 firstResponder,若是点击的是 view 的 subView,那么这个 subView 就是 firstResponder。若是点击了 view 以外的区域,那么这个 view 和它的 subView 都不会是 firstResponder(即便view的subView在view的Frame以外也不是)。代理
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
复制代码
在这个4个方法上面有一个基本注释:code
若是要处理本身的触控事件,那么就应该4个方法都覆盖。事件处理要么成功,要么失败。
orm
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
cdn
手指或者触控笔检测到一个触控事件会调用 touchesBegan。默认的实现以下
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
}
复制代码
它会直接把事件传递给下一个响应者。若是要处理自定义的触控事件就不要调用 super.touchesBegan(touches, with: event)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
和上面的 touchesBegan 同样,若是处理自定义触控就不要调用 super 方法了。当手指之类的触控事件移动的时候会调用这个方法。它会持续更新相同的移动的 UITouch 对象里面的数据。在 UITouch 里面你能够查看触发事件的 window 和当前的 View 相关属性。
你还能够查看 Touch 的当前情况:
public enum Phase : Int {
case began // 触摸开始
case moved // 接触点移动
case stationary // 接触点无移动
case ended // 触摸结束
case cancelled // 触摸取消
}
复制代码
一个简单的例子就是你能够实现一个 View 跟随手指移动效果。代码以下(不断更新 View 的中心点):
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let current = touch.location(in: self)
let previous = touch.previousLocation(in: self)
var currentCenter = center
currentCenter.x += current.x - previous.x
currentCenter.y += current.y - previous.y
self.center = currentCenter
}
}
复制代码
手指或者触控笔的触控事件结束
触控事件取消了。一些系统事件的打断会触发事件取消,好比此时电话来了。
view.isUserInteractionEnabled = false,关掉了事件响应功能。此时这个 view 和它的全部的 subView 都不会成为响应者
view.isHidden = true,view 都隐藏了,固然也就不会处理了
view.alpha = 0.0 ~ 0.01 透明度为0或者过小了。
或者不能正常调用 touchesEnded 处理,触控事件被 UIGestureRecognizer 对象截取(下面会说明)。
iOS 在好久之前其实也是用 touch 处理相关手势的,后来为了简便开发,因此就推出了 手势识别功能 UIGestureRecognizer
UIGestureRecognizer 是一个抽象类,实际使用的是它的子类(或者自定义子类)
下面是预约义的系统手势:
手势状态以下:
public enum State : Int {
case possible // 手势实际尚未识别,可是解析是 touch 事件。 默认状态
case began
case changed
case ended
case cancelled
case failed
}
复制代码
点击一个 view
private func gestureSetup() {
let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
colorView.backgroundColor = UIColor.red
view.addSubview(colorView)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(colorViewTap(gesture:)))
colorView.addGestureRecognizer(tapGesture)
}
@objc private func colorViewTap(gesture: UITapGestureRecognizer) {
switch gesture.state {
case .began:
print("tap began")
case .changed:
print("tap changed, move move move ....")
case .cancelled:
print("tap cancelled")
case .ended:
print("tap ended")
case .failed:
print("tap failed, not recognizer")
default:
print("tap default, it is possible enum state")
}
}
复制代码
正常成功操做输出:
tap ended
复制代码
长按一个 view
private func gestureSetup() {
let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
colorView.backgroundColor = UIColor.red
view.addSubview(colorView)
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(colorViewLongPress(gesture:)))
colorView.addGestureRecognizer(longPress)
}
@objc private func colorViewLongPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
print("long press began")
case .changed:
print("long press changed, move move move ....")
case .cancelled:
print("long press cancelled")
case .ended:
print("long press ended")
case .failed:
print("long press failed, not recognizer")
default:
print("long press default, it is possible enum state")
}
}
复制代码
正常成功操做输出:
long press began
long press changed, move move move ....
long press changed, move move move ....
...
long press changed, move move move ....
long press changed, move move move ....
long press ended
复制代码
轻扫手势有一个扫动方向 open var direction: UISwipeGestureRecognizer.Direction
,默认是向右扫
private func gestureSetup() {
let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
colorView.backgroundColor = UIColor.red
view.addSubview(colorView)
let swipePress = UISwipeGestureRecognizer(target: self, action: #selector(colorViewSwipe(gesture:)))
colorView.addGestureRecognizer(longPress)
}
@objc private func colorViewSwipe(gesture: UISwipeGestureRecognizer) {
if gesture.direction == .left {
print("swipe left")
} else if gesture.direction == .right {
print("swipe right")
} else if gesture.direction == .up {
print("swipe up")
} else if gesture.direction == .down {
print("swipe down")
}
}
复制代码
实现 view 的拖动
private func gestureSetup() {
let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
colorView.backgroundColor = UIColor.red
view.addSubview(colorView)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(colorViewPan(gesture:)))
colorView.addGestureRecognizer(panGesture)
}
@objc private func colorViewPan(gesture: UIPanGestureRecognizer) {
if gesture.state == .began {
print("pan began")
} else if gesture.state == .changed {
if let panView = gesture.view {
// 手势移动的 x和y值随时间变化的总平移量
let translation = gesture.translation(in: panView)
// 移动
panView.transform = panView.transform.translatedBy(x: translation.x, y: translation.y)
// 复位,至关于如今是起点
gesture.setTranslation(.zero, in: panView)
}
} else if gesture.state == .ended {
print("pan ended")
}
}
复制代码
实现 view 的缩放
private func gestureSetup() {
let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
colorView.backgroundColor = UIColor.red
view.addSubview(colorView)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(colorViewPinch(gesture:)))
colorView.addGestureRecognizer(pinchGesture)
}
@objc private func colorViewPinch(gesture: UIPinchGestureRecognizer) {
if let pinchView = gesture.view {
// 缩放
pinchView.transform = pinchView.transform.scaledBy(x: gesture.scale, y: gesture.scale)
// 复位
gesture.scale = 1
}
}
复制代码
实现 view 的旋转
private func gestureSetup() {
let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
colorView.backgroundColor = UIColor.red
view.addSubview(colorView)
let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(colorViewRotate(gesture:)))
colorView.addGestureRecognizer(rotateGesture)
}
@objc private func colorViewRotate(gesture: UIRotationGestureRecognizer) {
if let rotateView = gesture.view {
// 旋转
rotateView.transform = rotateView.transform.rotated(by: gesture.rotation)
// 复位
gesture.rotation = 0
}
}
复制代码
private func gestureSetup() {
let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
colorView.backgroundColor = UIColor.red
view.addSubview(colorView)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(colorViewPan(gesture:)))
let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(colorViewRotate(gesture:)))
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(colorViewTap(gesture:)))
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(colorViewLongPress(gesture:)))
let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(colorViewSwipe(gesture:)))
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(colorViewPinch(gesture:)))
// 设置 delegate 实现可识别多手势
swipeGesture.delegate = self
pinchGesture.delegate = self
longPressGesture.delegate = self
tapGesture.delegate = self
panGesture.delegate = self
rotateGesture.delegate = self
colorView.addGestureRecognizer(swipeGesture)
colorView.addGestureRecognizer(pinchGesture)
colorView.addGestureRecognizer(longPressGesture)
colorView.addGestureRecognizer(tapGesture)
colorView.addGestureRecognizer(rotateGesture)
colorView.addGestureRecognizer(panGesture)
}
extension ViewController: UIGestureRecognizerDelegate {
// 设置代理代表识别多个手势(默认 false)
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
复制代码
关键点是设置手势代理,实现多手势识别方法。
有下面一个需求 view 有一个 UITapGestureRecognizer, tableView 实现 tableView(didSelectRowAt:) 。
override func viewDidLoad() {
super.viewDidLoad()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction))
view.addGestureRecognizer(tapGesture)
}
@objc private func tapAction() {
print("tapAction")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "\(indexPath.row)"
cell.textLabel?.textColor = .white
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("didSelectRowAt indexPath = \(indexPath)")
}
复制代码
此时点击 tableView。didSelectRowAt
是不会调用的,调用的是 tapAction
方法。 由于 UIView 和 UITableView 都是 UIResponder 的子类。它们方法触控事件的调用遵循响应者链。 实际默认的调用顺序是:
didSelectRowAt
的正常响应被点击事件切断了,致使点击 tableView 取消了。要是打印输出,大体就是这样:
xxxxx tableView touchesBegan
xxxxx view touchesBegan
xxxxx view tapAction
xxxxx tableView touchesCancelled
复制代码
解决办法
UIGestureRecognizer 里面有 touch 事件的逻辑处理属性
open var cancelsTouchesInView: Bool // default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the view for all touches or presses recognized as part of this gesture immediately before the action method is called.
open var delaysTouchesBegan: Bool // default is NO. causes all touch or press events to be delivered to the target view only after this gesture has failed recognition. set to YES to prevent views from processing any touches or presses that may be recognized as part of this gesture
open var delaysTouchesEnded: Bool // default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized
复制代码
cancelsTouchesInView:手势识别成功之后是否取消 touch,默认为 true。
在上面的例子中因为既须要手势事件,也须要 touch 事件。因此设置 tapGesture.cancelsTouchesInView = false
就OK了。代表当识别成功手势之后不要取消 touch 事件的传递,此时 tableView 的点击就会正常运行了。
delaysTouchesBegan:是否延迟识别 touch。
默认为 false,代表先触发 touch 事件,而后判断手势是否识别成功。若是设置为 true,则若是此时有手势事件判断成功,手势成功就不会再调用 touchesBegan 事件了。
delaysTouchesEnded :是否延迟识别 touch。
大概逻辑也就是先识别手势,手势失败再正常调用 touchesEnded。
一个实际开发中不会用到的状况是:
private func buttonActionAndGesture() {
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
button.setTitle("Test", for: .normal)
view.addSubview(button)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
let buttonTap = UITapGestureRecognizer(target: self, action: #selector(buttonGesture))
button.addGestureRecognizer(buttonTap)
}
复制代码
此时会调用 button.addGestureRecognizer 手势事件,不会调用 button.addTarget 点击事件。因此 button.addTarget 应该是 touch 事件的一个解析,此时 touch 事件被 gesture 事件切断了(若有错误,欢迎指正😄)。