macOS控件的鼠標(biāo)懸停(Hover)操作實(shí)現(xiàn)

查看蘋果官方開(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ì)NSTextFieldNSView寨蹋、NSButton等等控件都需要實(shí)現(xiàn)鼠標(biāo)懸停的功能;那按照上面的方法就需要子類化所有控件扔茅、MouseMovableTextFiled已旧、MouseMovableViewMouseMovableButton; 顯然這樣做會(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è)屬性方法广匙;

源代碼:
https://github.com/jifucao/ViewHover

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末狈茉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子劣欢,更是在濱河造成了極大的恐慌,老刑警劉巖裁良,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凿将,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡价脾,警方通過(guò)查閱死者的電腦和手機(jī)牧抵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)侨把,“玉大人犀变,你說(shuō)我怎么就攤上這事∽叮” “怎么了弛作?”我有些...
    開(kāi)封第一講書人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)华匾。 經(jīng)常有香客問(wèn)我映琳,道長(zhǎng)机隙,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任萨西,我火速辦了婚禮有鹿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谎脯。我一直安慰自己葱跋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布源梭。 她就那樣靜靜地躺著娱俺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪废麻。 梳的紋絲不亂的頭發(fā)上荠卷,一...
    開(kāi)封第一講書人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音烛愧,去河邊找鬼油宜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛怜姿,可吹牛的內(nèi)容都是我干的慎冤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼沧卢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蚁堤!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起但狭,我...
    開(kāi)封第一講書人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤违寿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后熟空,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體藤巢,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年息罗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掂咒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡迈喉,死狀恐怖绍刮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情挨摸,我是刑警寧澤孩革,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站得运,受9級(jí)特大地震影響膝蜈,放射性物質(zhì)發(fā)生泄漏锅移。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一饱搏、第九天 我趴在偏房一處隱蔽的房頂上張望非剃。 院中可真熱鬧,春花似錦推沸、人聲如沸备绽。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肺素。三九已至,卻和暖如春宇驾,著一層夾襖步出監(jiān)牢的瞬間压怠,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工飞苇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蜗顽。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓布卡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親雇盖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忿等,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容