macOS應用開發基礎之Popover mac中安裝了一個叫 Pap.er 的app,用來下載和管理桌面壁紙,壁紙資源質量很高幺,app不但很輕量(僅有5MB)而且設計精美,還免費的(我這算在幫他們做推廣嗎^_^,好的東西就應該推廣一下)。這款app採用了狀態欄小工具的形式,界面在 Popover 中實現。看視頻感受一下,叫Popover的彈窗,就是本文要講的東西。Pap.er - 專為 Mac 設計的壁紙應用?paper.meiyuan.in00:09展示 Popover 的效果 今天以一個簡單的實例分享一下如何實現 Popover 彈窗。 1. 平台 macOS 10.13.5 Xcode 9.4.1 swift 4.1.2 本文基於上述平台實現,下面的代碼中可能隨著 Swift 語言的版本更新會需要調整(不過應該不多),Xcode n會比較智能的提出修改建議,視情況調整即可,不過實現思路是一致的。 2. 新建及配置工程 打開xcode新建工程, macOS -> Cocoa App -> Next: 輸入工程名稱:PopoverDemo,語言選擇Swift,勾掉Storyboard: Next,點擊 create 即可打開創建的新工程; 點擊運行按鈕,可以看到程序運行,出現一個空的窗口,同時dock上出現了應用圖標,這不是我們想要的,設置一下不顯示它們: 工程導航欄選中工程PopoverDemo,打開Info標籤頁; 可以看到Custom macOS application Target Properties組,添加新的配置Application is agent(UI Element),布爾屬性,值為 YES: 重新運行程序,可以看到已經不顯示Dock圖標; 打開文件MainMenu.xib,可以看到界面設計中有Window 和 MainMenu,兩個選中刪掉; 打開文件AppDelegate.swift,刪掉以下代碼: @IBOutlet weak var window: NSWindow! 再次運行程序,主窗口也不顯示了,連菜單欄也木有了,不要著急,咱繼續。 3. 添加狀態欄按鈕 打開文件AppDelegate.swift,在類中添加屬性,這一步是創建一個狀態欄按鈕,設置寬度屬性NSStatusItem.squareLength,代碼如下:let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) 狀態欄按鈕總該需要一個圖標吧!打開Assets.xcassets,右擊顯示AppIcon下方的空白區,選擇New Image Set,重命名為statusIcon,當然這個名字隨便定,選中這個圖集,會看到右側有配置區,配置圖集按照Template Image渲染: 看到有三個虛線框空白區,這就是圖片區,狀態欄按鈕的圖片基本大小為 18px x 18px ,還需2倍和3倍的適用於視網膜屏幕的 mac,像素分別是 36px x 36px 和 54px x 54px ,可以使用以下我提供的圖標: 分別將圖拖到對應位置: 切換到文件AppDelegate.swift,定義一個測試狀態欄按鈕點擊行為的函數,這裡以關閉應用程序為例吧,實現的函數: @objc func quitApp(_ sender: AnyObject) { NSApplication.shared.terminate(self) } 然後找到applicationDidFinishLaunching在其中添加以下代碼,為狀態欄按鈕配置圖標和行為:if let button = statusItem.button { button.image = NSImage(named: NSImage.Name("statusIcon")) button.action = #selector(quitApp) } 此時運行程序會看到狀態欄中出現了我們定義的按鈕,點擊一下,應用程序就退出了。 前面設置圖片集渲染方式為Template Image,是為了適配不同的狀態欄主題,因為macOS還有個暗黑主題不是? 兩種主題下的效果如下: 4. 添加Popover 4.1 添加Popover控制項 打開文件MainMenu.xib,右下腳搜索控制項Popover就會看到: 點擊控制項將其拖入界面,添加後其並沒有可視化的元素,可以在Objects管理器中看到已經添加成功: 啟動Assitant Editor,按住Contorl鍵點擊Popover拖入AppDelegate.swift文件,創建popover屬性: 00:10拖拽創建屬性 4.2 添加Popover View Controller 此時popover是沒有界面的,因為此時還沒有為其指定view controller。 Command+n或者菜單欄依次選擇File->New->File…,就會調出新建文件窗口,選擇 macOS -> Cocoa Class -> Next; 名稱最好是跟目的統一,這裡我設置成PopoverDemoViewController,繼承自NSViewController,勾選??also create XIB file for user interface,語言依舊是Swift,然後 Next -> Create會創建兩個文件。 PopoverDemoViewController.swift PopoverDemoViewController.xib 打開文件MainMenu.xib,選擇界面設計中的Popover View Controller,然後設置其對應的類為剛才創建的PopoverDemoViewController,此時popover的界面就可以在PopoverDemoViewController.xib中設計了: 此時雖指定了具體的 view controller,但還沒有觸發popover顯示的地方,一開始我們添加了 statusItem,就是為了利用狀態欄按鈕點擊來顯示的,只需要定義開關popover的介面指定給statusItem的action即可,定義以下三個函數: @objc func showPopover(_ sender: AnyObject) { if let button = statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) } } @objc func closePopover(_ sender: AnyObject) { popover.performClose(sender) } @objc func togglePopover(_ sender: AnyObject) { if popover.isShown { closePopover(sender) } else { showPopover(sender) } } 重新更改一下applicationDidFinishLaunching中實現的statusItem的action為上面的togglePopover,刪掉之前定義的quitApp就可以了: if let button = statusItem.button { button.image = NSImage(named: NSImage.Name("statusIcon")) button.action = #selector(togglePopover) } 此時運行程序,就會看到正常出現的狀態欄按鈕,點擊按鈕就會彈出Popover,再次點擊就會關閉。 4.3 設計Popover界面 上面提到,我們可以在PopoverDemoViewController.xib中設計popover的界面。 4.3.1 添加應用退出按鈕 打開PopoverDemoViewController.xib文件,會看到一個view控制項,我們拖動一個Push Button控制項到view中,放置到右上角。 選中添加的按鈕,在控制項屬性窗口中, 取消勾選 Boarded,選擇Image 為 NSStopProcessFrestandingTemplate: 調出Assitant Editor,按住Control鍵,點擊按鈕拖入PopoverDemoViewContoller.swift,創建action為點擊事件quitApp,實現代碼與之前的測試函數quiApp一樣: @IBAction func quitApp(_ sender: Any) { NSApplication.shared.terminate(self) } 運行程序,點擊彈出popover,此時可以看到剛才添加的按鈕,點擊一下按鈕,程序就會退出。 4.3.2 添加無用的標籤 總要在程序中顯示點東西吧,就像添加按鈕一樣拖動一個Label控制項到view中,內容改為經典的Hello, World!,啊,不行太俗了,還是改為Hello, Popover!吧,然後調整標籤大小,並調整位置在水平和垂直居中的位置,調整內容居中: 運行程序,看到想要的效果! 5. 優化Popover 此時運行,你會發現有一個問題:點擊彈窗外面,彈窗不會自動收起。這並不是我們想要的,查看apple官方的NSPopover文檔,我們知道他有一個behavior屬性,其值為NSPopover.Behavior.transient的時候好像可以實現,嘗試一下。打開MainMenu.xib,選中Popover,在其屬性設置區就會看到Behavior,我們選擇Transient,運行程序會發現:確實可以實現,點擊彈窗外面,彈窗會自動收起,但是前提是必須在彈窗內有一次點擊事件後才能做到這個效果。 後來發現若彈窗內有 firs responder 就可以實現理想結果,不用操作彈窗中的內容,在彈窗外點擊就會收起彈窗! 顯然上面的配置也不是我們想要的,網上看到一種神奇的方式:添加系統事件監視器來實現對交互事件的監測,從而做到彈窗顯示後,無論什麼時候點擊彈窗外面都能收起彈窗的效果。 新建名為EventMonitor的swift文件:Command+n組合拳,選擇macOS-> Swift File -> Next,輸入文件名EventMonitor創建; 文件代碼為: import Cocoa class EventMonitor { var mask: NSEvent.EventTypeMask var handler : (NSEvent?) -> () var monitor: Any? init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> ()){ self.mask = mask self.handler = handler } deinit { stop() } func start(){ monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) } func stop() { if monitor != nil { NSEvent.removeMonitor(monitor!) monitor = nil } } } 文件中定義了EventMonitor類,添加了構造函數和兩個介面用於,創建用戶操作事件監視器、啟動和關閉監視器。 打開文件AppDelegate.swift,添加監視器屬性: var eventMonitor: EventMonitor? 在applicationDidFinishLaunching中添加監視器的初始化操作: eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let strongSelf = self, strongSelf.popover.isShown { strongSelf.closePopover(event!) } } 還需要完善popover顯示和關閉的介面: @objc func closePopover(_ sender: AnyObject) { popover.performClose(sender) eventMonitor?.stop() } @objc func showPopover(_ sender: AnyObject) { if let button = statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) } eventMonitor?.start() } 最後,最好是打開MainMenu.xib文件將Popover的Behavior屬性設置為Applicationed Defined。運行程序,噹啷啷,符合預期! 6. 運行效果 最終運行效果:00:07Popover 實例最終效果完整的工程可以點擊下面卡片下載:點我?pichome-1254392422.cos.ap-chengdu.myqcloud.com 參考: Menus and Popovers in Menu Bar Apps for macOS?www.raywenderlich.comAppKit | Apple Developer Documentation?developer.apple.comAppKit | Apple Developer Documentation?developer.apple.com 博客原文:macOS應用開發基礎之Popover?www.smslit.top 推薦閱讀: 相关文章 {{#data}} {{title}} {{/data}}