查看蘋果官方開(kāi)發(fā)文檔會(huì)發(fā)現(xiàn)NSView有兩個(gè)方法鼠標(biāo)進(jìn)入mouseEntered(with:)
和鼠標(biāo)退出mouseExited(with:)
兩個(gè)方法脱衙;
雖然文檔上說(shuō):子類覆寫這兩個(gè)方法可以接受到對(duì)應(yīng)的事件侥猬;但實(shí)際上還需要override updateTrackingAreas
方法并且更新對(duì)應(yīng)的trackingAreas
具體實(shí)現(xiàn)的代碼如下
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))
}
}
實(shí)現(xiàn)懸停的邏輯是鼠標(biāo)進(jìn)入時(shí)改變按鈕樣式或者展示氣泡;鼠標(biāo)退出時(shí)恢復(fù)樣式或者氣泡消失捐韩;
為了實(shí)現(xiàn)視圖跟業(yè)務(wù)邏輯代碼分離需要定義一個(gè)鼠標(biāo)移動(dòng)事件退唠;
定義一個(gè)MouseMoveEvent
的枚舉類型;并且增加subscribeMouseMoveEvent
的事件訂閱方法便可將業(yè)務(wù)代碼和UI視圖的代碼分離開(kāi)來(lái)荤胁;具體實(shí)現(xiàn)如下
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
}
}
到目前為止鼠標(biāo)懸停的目的已經(jīng)達(dá)到了铜邮;但是有一個(gè)小問(wèn)題:假設(shè)項(xiàng)目中有需求對(duì)NSTextField
、NSView
寨蹋、NSButton
等等控件都需要實(shí)現(xiàn)鼠標(biāo)懸停的功能;那按照上面的方法就需要子類化所有控件扔茅、MouseMovableTextFiled
已旧、MouseMovableView
、MouseMovableButton
; 顯然這樣做會(huì)出現(xiàn)大量重復(fù)的模版代碼召娜;
使用協(xié)議實(shí)現(xiàn)鼠標(biāo)懸停接口
如果可以定義一個(gè)協(xié)議MouseTrackable
來(lái)實(shí)現(xiàn)鼠標(biāo)懸停的接口运褪;那么只要實(shí)現(xiàn)此協(xié)議的控件就能獲得鼠標(biāo)懸停的能力extension NSView: MouseTrackable {}
;代碼實(shí)現(xiàn)如下
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
}
}
使用協(xié)議實(shí)現(xiàn)鼠標(biāo)懸停接口的好處就是不用對(duì)目標(biāo)控件進(jìn)行子類化玖瘸;通過(guò)對(duì)NSView
拓展出mouseTracker
對(duì)象后便可以簡(jiǎn)單的實(shí)現(xiàn)鼠標(biāo)移動(dòng)事件的訂閱秸讹;當(dāng)然NSButton、NSImageView捕捂、NSTextField也會(huì)自動(dòng)繼承這個(gè)屬性方法广匙;