mac中安裝了一個叫 Pap.er 的app,用來下載和管理桌面壁紙,壁紙資源質量很高幺,app不但很輕量(僅有5MB)而且設計精美,還免費的(我這算在幫他們做推廣嗎^_^,好的東西就應該推廣一下)。這款app採用了狀態欄小工具的形式,界面在 Popover 中實現。看視頻感受一下,叫Popover的彈窗,就是本文要講的東西。
今天以一個簡單的實例分享一下如何實現 Popover 彈窗。
本文基於上述平台實現,下面的代碼中可能隨著 Swift 語言的版本更新會需要調整(不過應該不多),Xcode n會比較智能的提出修改建議,視情況調整即可,不過實現思路是一致的。
PopoverDemo
Swift
Storyboard
Info
Custom macOS application Target Properties
Application is agent(UI Element)
MainMenu.xib
AppDelegate.swift
@IBOutlet weak var window: NSWindow!
打開文件AppDelegate.swift,在類中添加屬性,這一步是創建一個狀態欄按鈕,設置寬度屬性NSStatusItem.squareLength,代碼如下:
NSStatusItem.squareLength
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
狀態欄按鈕總該需要一個圖標吧!打開Assets.xcassets,右擊顯示AppIcon下方的空白區,選擇New Image Set,重命名為statusIcon,當然這個名字隨便定,選中這個圖集,會看到右側有配置區,配置圖集按照Template Image渲染:
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在其中添加以下代碼,為狀態欄按鈕配置圖標和行為:
applicationDidFinishLaunching
if let button = statusItem.button { button.image = NSImage(named: NSImage.Name("statusIcon")) button.action = #selector(quitApp) }
此時運行程序會看到狀態欄中出現了我們定義的按鈕,點擊一下,應用程序就退出了。
前面設置圖片集渲染方式為Template Image,是為了適配不同的狀態欄主題,因為macOS還有個暗黑主題不是?
兩種主題下的效果如下:
打開文件MainMenu.xib,右下腳搜索控制項Popover就會看到:
Popover
點擊控制項將其拖入界面,添加後其並沒有可視化的元素,可以在Objects管理器中看到已經添加成功:
Objects
啟動Assitant Editor,按住Contorl鍵點擊Popover拖入AppDelegate.swift文件,創建popover屬性:
Assitant Editor
Contorl
popover
00:10拖拽創建屬性
此時popover是沒有界面的,因為此時還沒有為其指定view controller。
view controller
Command
n
PopoverDemoViewController
NSViewController
also create XIB file for user interface
Popover View Controller
PopoverDemoViewController.xib
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) } }
togglePopover
quitApp
if let button = statusItem.button { button.image = NSImage(named: NSImage.Name("statusIcon")) button.action = #selector(togglePopover) }
上面提到,我們可以在PopoverDemoViewController.xib中設計popover的界面。
Control
PopoverDemoViewContoller.swift
quiApp
@IBAction func quitApp(_ sender: Any) { NSApplication.shared.terminate(self) }
總要在程序中顯示點東西吧,就像添加按鈕一樣拖動一個Label控制項到view中,內容改為經典的Hello, World!,啊,不行太俗了,還是改為Hello, Popover!吧,然後調整標籤大小,並調整位置在水平和垂直居中的位置,調整內容居中:
Label
view
Hello, World!
Hello, Popover!
運行程序,看到想要的效果!
此時運行,你會發現有一個問題:點擊彈窗外面,彈窗不會自動收起。這並不是我們想要的,查看apple官方的NSPopover文檔,我們知道他有一個behavior屬性,其值為NSPopover.Behavior.transient的時候好像可以實現,嘗試一下。
NSPopover
behavior
NSPopover.Behavior.transient
打開MainMenu.xib,選中Popover,在其屬性設置區就會看到Behavior,我們選擇Transient,運行程序會發現:確實可以實現,點擊彈窗外面,彈窗會自動收起,但是前提是必須在彈窗內有一次點擊事件後才能做到這個效果。
Behavior
Transient
後來發現若彈窗內有 firs responder 就可以實現理想結果,不用操作彈窗中的內容,在彈窗外點擊就會收起彈窗!
顯然上面的配置也不是我們想要的,網上看到一種神奇的方式:添加系統事件監視器來實現對交互事件的監測,從而做到彈窗顯示後,無論什麼時候點擊彈窗外面都能收起彈窗的效果。
EventMonitor
swift
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類,添加了構造函數和兩個介面用於,創建用戶操作事件監視器、啟動和關閉監視器。
var eventMonitor: EventMonitor?
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let strongSelf = self, strongSelf.popover.isShown { strongSelf.closePopover(event!) } }
@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() }
Applicationed Defined
最終運行效果:
完整的工程可以點擊下面卡片下載:
Menus and Popovers in Menu Bar Apps for macOS?www.raywenderlich.comAppKit | Apple Developer Documentation?developer.apple.comAppKit | Apple Developer Documentation?developer.apple.com
博客原文:
推薦閱讀: