mac中安裝了一個叫 Pap.er 的app,用來下載和管理桌面壁紙,壁紙資源質量很高幺,app不但很輕量(僅有5MB)而且設計精美,還免費的(我這算在幫他們做推廣嗎^_^,好的東西就應該推廣一下)。這款app採用了狀態欄小工具的形式,界面在 Popover 中實現。看視頻感受一下,叫Popover的彈窗,就是本文要講的東西。

Pap.er - 專為 Mac 設計的壁紙應用?

paper.meiyuan.in

視頻封面

00: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,可以看到界面設計中有WindowMainMenu,兩個選中刪掉;
  • 打開文件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 36px54px 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的介面指定給statusItemaction即可,定義以下三個函數:

@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中實現的statusItemaction為上面的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 就可以實現理想結果,不用操作彈窗中的內容,在彈窗外點擊就會收起彈窗!

顯然上面的配置也不是我們想要的,網上看到一種神奇的方式:添加系統事件監視器來實現對交互事件的監測,從而做到彈窗顯示後,無論什麼時候點擊彈窗外面都能收起彈窗的效果。

  • 新建名為EventMonitorswift文件: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文件將PopoverBehavior屬性設置為Applicationed Defined。運行程序,噹啷啷,符合預期!

6. 運行效果

最終運行效果:

視頻封面

00:07Popover 實例最終效果

完整的工程可以點擊下面卡片下載:

點我?

pichome-1254392422.cos.ap-chengdu.myqcloud.com

參考:

Menus and Popovers in Menu Bar Apps for macOS?

www.raywenderlich.com圖標AppKit | Apple Developer Documentation?

developer.apple.com

AppKit | Apple Developer Documentation?

developer.apple.com


博客原文:

macOS應用開發基礎之Popover?

www.smslit.top
圖標

推薦閱讀:

相关文章