最近在使用NSPressGestureRecognizer
处理长按的时候发现了问题。如果弹出的视图控制器,是采用的show
方式,则一切正常。但如果是使用modal
的方式,则无法识别长按。
代码如下:
import Cocoa
class VC: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = NSView(frame: NSRect(x: 100, y: 100, width: 100, height: 100))
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.systemBrown.cgColor
let longPress = NSPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
view.addGestureRecognizer(longPress)
self.view.addSubview(view)
}
@objc func longPress(_ sender:Any) {
print("long")
}
}
分析
我自己弄了半天,没能找到解决方案,于是跑到SO上问。NSPressGestureRecognizer doesn't work in modal ViewController。一觉睡醒,发现已经有人回答了。
Interesting. What seems to be happening is that the recognizer state never changes from possible to "began" in the modal example.
class Recognizer: NSPressGestureRecognizer {
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
self.state = .began
}
}
解答的代码虽然不完整,但是至少提供了一个方向。按照解答的思路,我重新实现了NSPressGestureRecognizer
。
模拟的目标是实现在show
方式下同样的状态改变。即在正常长按的情况下,依次实现possible、began、end。使用一个Timer
,在满足长按时间的情况下,发送began,并且在用户抬起鼠标且有began的情况下,发送end。
import Cocoa
class VC: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = NSView(frame: NSRect(x: 100, y: 100, width: 100, height: 100))
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.systemBrown.cgColor
let longPress = MyPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
view.addGestureRecognizer(longPress)
let click = NSClickGestureRecognizer(target: self, action: #selector(click(_:)))
view.addGestureRecognizer(click)
self.view.addSubview(view)
}
@objc func click(_ sender:Any) {
print("click")
}
@objc func longPress(_ sender:Any) {
guard let gesture = sender as? NSGestureRecognizer else { return }
switch gesture.state {
case .ended:
print("long")
default:
print(gesture.state)
}
}
}
class MyPressGestureRecognizer: NSPressGestureRecognizer {
private weak var timer:Timer? = nil
private var hasBegan = false
private var hasCancelled = false
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
timer = Timer.scheduledTimer(withTimeInterval: minimumPressDuration, repeats: false) { (timer) in
defer {
timer.invalidate()
}
DispatchQueue.main.async {
self.state = .began
self.hasBegan = true
}
}
}
override func mouseUp(with event: NSEvent) {
if hasBegan {
self.state = .ended
self.hasBegan = false
}
super.mouseUp(with: event)
}
override func reset() {
timer?.invalidate()
super.reset()
}
}
extension NSGestureRecognizer.State:CustomStringConvertible {
public var description:String {
switch self {
case .possible:
return "possible"
case .began:
return "began"
case .changed:
return "changed"
case .ended:
return "ended"
case .cancelled:
return "cancelled"
case .failed:
return "failed"
@unknown default:
return "default"
}
}
}
运行之后,发现我实现的代码,和苹果原本的
NSPressGestureRecognizer
,效果完全一样。也就是说,我的代码同样有模态方式下,无法识别长按的问题。
再次思考,我发现问题出现Timer
上,在模态运行的视图控制器,Timer
不会执行。我猜,苹果大概也是代码中使用了Timer
,才会有同样的问题。
解决
最终的方案是不使用Timer
,增加一个是否取消的参数进行判断。代码如下:
import Cocoa
class VC: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = NSView(frame: NSRect(x: 100, y: 100, width: 100, height: 100))
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.systemBrown.cgColor
let longPress = MyPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
view.addGestureRecognizer(longPress)
let click = NSClickGestureRecognizer(target: self, action: #selector(click(_:)))
view.addGestureRecognizer(click)
self.view.addSubview(view)
}
@objc func click(_ sender:Any) {
print("click")
}
@objc func longPress(_ sender:Any) {
guard let gesture = sender as? NSGestureRecognizer else { return }
switch gesture.state {
case .ended:
print("long")
default:
print(gesture.state)
}
}
}
class MyPressGestureRecognizer: NSPressGestureRecognizer {
private var hasBegan = false
private var hasCancelled = false
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
hasCancelled = false
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(minimumPressDuration * 1000))) {
if !self.hasCancelled {
self.state = .began
self.hasBegan = true
}
}
}
override func mouseUp(with event: NSEvent) {
if hasBegan {
self.state = .ended
self.hasBegan = false
} else {
self.hasCancelled = true
}
super.mouseUp(with: event)
}
}
extension NSGestureRecognizer.State:CustomStringConvertible {
public var description:String {
switch self {
case .possible:
return "possible"
case .began:
return "began"
case .changed:
return "changed"
case .ended:
return "ended"
case .cancelled:
return "cancelled"
case .failed:
return "failed"
@unknown default:
return "default"
}
}
}
总结
Timer
在模态时会失效。我们在使用时需要小心。- 上面的代码只处理了鼠标左键,如果想处理其它按键的长按,也可以用同样的方式进行处理。
参考
NSPressGestureRecognizer doesn't work in modal ViewController