macOS控件的鼠标悬停(Hover)操作实现
2022-01-20 本文已影响0人
jifu
查看苹果官方开发文档会发现NSView有两个方法鼠标进入mouseEntered(with:)
和鼠标退出mouseExited(with:)
两个方法;
虽然文档上说:子类覆写这两个方法可以接受到对应的事件;但实际上还需要override updateTrackingAreas
方法并且更新对应的trackingAreas
具体实现的代码如下
class MouseTrackingView: NSView {
var isMouseInside: Bool = false {
didSet {
print("isMouseInside:\(isMouseInside)")
}
}
override func mouseExited(with event: NSEvent) {
isMouseInside = false
}
override func mouseEntered(with event: NSEvent) {
isMouseInside = true
}
override func updateTrackingAreas() {
trackingAreas.forEach(removeTrackingArea(_:))
addTrackingArea(.init(rect: .zero,
options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited],
owner: self,
userInfo: nil))
}
}
实现悬停的逻辑是鼠标进入时改变按钮样式或者展示气泡;鼠标退出时恢复样式或者气泡消失;
为了实现视图跟业务逻辑代码分离需要定义一个鼠标移动事件;
定义一个MouseMoveEvent
的枚举类型;并且增加subscribeMouseMoveEvent
的事件订阅方法便可将业务代码和UI视图的代码分离开来;具体实现如下
class MouseTrackingView: NSView {
enum MouseMoveEvent {
case exited, entered
}
typealias MouseMoveEventObserver = (MouseMoveEvent) -> Void
private var observer: MouseMoveEventObserver?
var mouseMoveEvent: MouseMoveEvent = .exited {
didSet {
observer?(mouseMoveEvent)
}
}
override func mouseExited(with event: NSEvent) {
mouseMoveEvent = .exited
}
override func mouseEntered(with event: NSEvent) {
mouseMoveEvent = .entered
}
override func updateTrackingAreas() {
trackingAreas.forEach(removeTrackingArea(_:))
addTrackingArea(.init(rect: .zero,
options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited],
owner: self,
userInfo: nil))
}
func subscribeMouseMoveEvent(_ observer: MouseMoveEventObserver?) {
self.observer = observer
}
}
使用方法:
@IBOutlet weak var mouseView: MouseTrackingView!
mouseView.subscribeMouseMoveEvent { event in
print("mouse event: \(event)")
switch event {
case .exited: break
case .entered: break
}
}
到目前为止鼠标悬停的目的已经达到了;但是有一个小问题:假设项目中有需求对NSTextField
、NSView
、NSButton
等等控件都需要实现鼠标悬停的功能;那按照上面的方法就需要子类化所有控件、MouseMovableTextFiled
、MouseMovableView
、MouseMovableButton
; 显然这样做会出现大量重复的模版代码;
使用协议实现鼠标悬停接口
如果可以定义一个协议MouseTrackable
来实现鼠标悬停的接口;那么只要实现此协议的控件就能获得鼠标悬停的能力extension NSView: MouseTrackable {}
;代码实现如下
import AppKit
enum MouseMoveEvent {
case exited, entered
}
// MARK: -
protocol MouseTrackable {
func subscribeMouseMoveEvent(_ observer: @escaping (MouseMoveEvent) -> Void)
}
// MARK: -
protocol MouseTrackCompatible {
var mouseTracker: MouseTrackable { get }
}
// MARK: -
class MouseTracker: MouseTrackable {
typealias MouseMoveEventObserver = (MouseMoveEvent) -> Void
private var observer: MouseMoveEventObserver?
private var event: MouseMoveEvent = .exited {
didSet {
observer?(event)
}
}
func subscribeMouseMoveEvent(_ observer: @escaping MouseMoveEventObserver) {
self.observer = observer
}
func updateMouseMoveEvent(_ event: MouseMoveEvent) {
self.event = event
}
}
// MARK: -
extension NSView: MouseTrackCompatible {
static var _mouseTracker: Int = 0
var mouseTracker: MouseTrackable {
guard let tracker = objc_getAssociatedObject(self, &Self._mouseTracker) as? MouseTracker else {
let tracker = MouseTracker()
objc_setAssociatedObject(self, &Self._mouseTracker, tracker, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
exchangeUpdateTrackingAreasImplementation()
return tracker
}
return tracker
}
open override func mouseExited(with event: NSEvent) {
(mouseTracker as? MouseTracker)?.updateMouseMoveEvent(.exited)
}
open override func mouseEntered(with event: NSEvent) {
(mouseTracker as? MouseTracker)?.updateMouseMoveEvent(.entered)
}
private func exchangeUpdateTrackingAreasImplementation() {
let classObj = Self.self
let origin = class_getInstanceMethod(classObj, #selector(updateTrackingAreas))!
let new = class_getInstanceMethod(classObj, #selector(swizzle_updateTrackingAreas))!
method_exchangeImplementations(origin, new)
}
@objc
private func swizzle_updateTrackingAreas() {
swizzle_updateTrackingAreas()
trackingAreas.forEach(removeTrackingArea(_:))
addTrackingArea(.init(rect: .zero,
options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited],
owner: self,
userInfo: nil))
}
}
使用方法:
self.label
.mouseTracker
.subscribeMouseMoveEvent { event in
print("mouse event: \(event)")
switch event {
case .exited: break
case .entered: break
}
}
使用协议实现鼠标悬停接口的好处就是不用对目标控件进行子类化;通过对NSView
拓展出mouseTracker
对象后便可以简单的实现鼠标移动事件的订阅;当然NSButton、NSImageView、NSTextField也会自动继承这个属性方法;