菜單欄 app 很久之前就已經(jīng)成為 OS X 的重要組件顷蟆。例如 [1Password]( Features - 1Password ) 和 [Day One]( Day One | A simple and elegant journal for iPhone, iPad, and Mac. ) 有菜單欄 app 作為組件者疤。另外一些比如 [Fantastical]( Flexibits | Fantastical 2 for Mac | Meet your Mac’s new calendar. ) 就只生存在 OS X 的菜單欄里端圈。
本教程會(huì)建立一個(gè)菜單欄 app漫试,在 popover 中顯示名人名言〕用可以在其中學(xué)到:
- 如何創(chuàng)建菜單欄圖標(biāo)
- 如何讓 app 只存活在菜單欄了里
- 如何為用戶添加一個(gè)菜單
- 如何在用戶需要的時(shí)候顯示彻秆、用戶離開的時(shí)候隱藏 popover —— 也叫事件監(jiān)督(Event Monitoring)
- 如何添加基本 UI 元素
注意:本教程假設(shè)你熟知 Swift 和 OS X。如果你需要學(xué)習(xí)蛇耀,就看 Getting Started With OS X and Swift 教程辩诞。
上手
打開 Xcode纺涤。選擇 File/New/Project… 然后選擇 OS X/Application/Cocoa Application 模板然后點(diǎn)擊 Next译暂。
在下一屏,Product Name 輸入 ** Quotes**撩炊,輸入必要的 Organization Name 和 Organization Identifier秧秉。然后確定選擇了 Swift 語言,取消 Use Storyboards, Create Document-Based Application 和 Use Core Data 的勾選衰抑。
最后象迎,再次點(diǎn)擊 Next,選擇一個(gè)位置來保存項(xiàng)目然后點(diǎn)擊 Create呛踊。
注意:在 iOS 以及 OS X Yosemite 上你應(yīng)該優(yōu)先使用 storyboard砾淌。但在這個(gè)例子里,使用 storyboard 會(huì)讓只生存在菜單欄里的 app 變得更復(fù)雜谭网。所以你要用 xib 來構(gòu)建 app 的用戶界面汪厨。
新項(xiàng)目建立好之后,打開 AppDelegate.swift 然后給類添加下面這個(gè) property:
let statusItem = NSStatusBar.system().statusItem(withLength: NSSquareStatusItemLength)
這樣就在菜單欄用固定長(zhǎng)度創(chuàng)建了一個(gè) Status Item —— 也叫作應(yīng)用程序圖標(biāo)愉择,用戶能看見以及使用它劫乱。
下一步,需要給 status item 配一張圖片锥涕,讓 app 在菜單欄可以被識(shí)別出來衷戈。
打開 Images.xcassets。然后下載這個(gè)圖片 StatusBarButtonImage@2x.png层坠,把它拖到 asset catelog里殖妇。
選擇圖片然后打開 attributes inspector。設(shè)置 Devices 為 Device Specific破花,然后確定 Mac 選項(xiàng)是選上的谦趣。改變 Render As 選項(xiàng)為 Template Image。
如果你要使用自定義的圖片座每,確保圖片是黑白的前鹅,并且配置為 template image,這樣 Status Item 在 light 和 dark 模式下看起來都很完美峭梳。
回到 AppDelegate.swift舰绘,然后把下面的代碼添加到 applicationDidFinishLaunching(_:):
if let button = statusItem.button {
button.image = NSImage(named: "StatusBarButtonImage")
button.action = Selector("printQuote:")
}
這樣就用剛剛添加的圖片作為圖標(biāo)配置了 status item,點(diǎn)擊 item 的時(shí)候也會(huì)有一個(gè)動(dòng)作。
在你測(cè)試 app 之前除盏,需要添加那個(gè)按鈕方法叉橱。把下面的方法添加到類里:
func printQuote(sender: AnyObject) {
let quoteText = "Never put off until tomorrow what you can do the day after tomorrow."
let quoteAuthor = "Mark Twain"
println("\(quoteText) — \(quoteAuthor)")
}
這個(gè)方法會(huì)輸出一條簡(jiǎn)單的馬克·吐溫名言到控制臺(tái)里。
編譯運(yùn)行 app者蠕,就能看到一個(gè)可用的新菜單欄 app 了窃祝。你做到了!
注意:如果你有太多菜單欄 app踱侣,可能就沒法看到自己的那個(gè)了粪小。切換到菜單比 Xcode 少的 app(例如 Finder)你應(yīng)該就能看到了。
每次點(diǎn)擊菜單欄圖標(biāo)抡句,就會(huì)看見 Xcode 控制臺(tái)輸出了名言探膊。
隱藏 Dock 圖標(biāo)和主窗口
在成為一個(gè)有用的菜單欄 app 之前,還要做兩件小事:隱藏 dock 圖標(biāo)并且干掉主窗口待榔。
要禁用 dock 圖標(biāo)逞壁,打開 Info.plist。然后添加一個(gè)新鍵 Application is agent (UIElement) 然后設(shè)置值為 YES锐锣。
注意:如果你很擅長(zhǎng)編輯 plist 文件腌闯,也可以手動(dòng)把鍵設(shè)置為 ** LSUIElement**。
現(xiàn)在是時(shí)候修理主窗口了雕憔。打開 MainMenu.xib 然后選擇窗口對(duì)象姿骏。然后,在 attributes inspector 里設(shè)置窗口斤彼,讓它在啟動(dòng)的時(shí)候不可見分瘦。
編譯運(yùn)行。你會(huì)看到 app 沒有主窗口了琉苇,也沒有討厭的 dock 圖標(biāo)嘲玫,只有一個(gè)可愛的 status item 在菜單欄里!
給 Status Item 添加一個(gè)菜單
一般情況下翁潘,菜單欄 app 只有一次少得可憐的點(diǎn)擊是不夠用的趁冈。添加更多功能最賤的方式就是增加一個(gè)菜單歼争。把下面的代碼添加到 applicationDidFinishLaunching(_:) 的結(jié)尾:
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Print Quote", action: Selector("printQuote:"), keyEquivalent: "P"))
menu.addItem(NSMenuItem.separatorItem())
menu.addItem(NSMenuItem(title: "Quit Quotes", action: Selector("terminate:"), keyEquivalent: "q"))
statusItem.menu = menu
這樣就創(chuàng)建了一個(gè) NSMenu拜马,給它增加了幾個(gè) NSMenuItem 的實(shí)例,然后設(shè)置 status item 的菜單為這個(gè)新的菜單沐绒。
這里需要注意幾點(diǎn):
- menu item 的 title 很明顯俩莽;就是顯示在 menu item 上的文字。
- action乔遮,就像按鈕或任意其它控件的 action扮超,點(diǎn)擊 menu item 的時(shí)候會(huì)調(diào)用的那個(gè)方法。
- ** KeyEquivalent** 是快捷鍵,可以用來激活 menu item出刷。小寫表示使用 Cmd 作為輔助鍵璧疗,大寫表示使用 Cmd+Shift。這個(gè)鍵盤快捷鍵只在應(yīng)用在最前端并且活動(dòng)的情況下有效馁龟。所以崩侠,在這個(gè)例子里,menu 或所有其它窗口需要是可以被看見的坷檩,因?yàn)檫@個(gè) app 沒有 dock 圖標(biāo)却音。
- ** separatorItem** 是一個(gè)處于非激活狀態(tài)的 menu item,在其它 menu item 之間顯示為一條簡(jiǎn)單的灰線矢炼。用它來給菜單里的功能分組系瓢。
- printQuote: 動(dòng)作是已經(jīng)在 AppDelegate 里定義好的方法。對(duì)于另一個(gè)句灌,terminate: 是定義在 shared application instance 里的動(dòng)作方法夷陋。因?yàn)槟銢]有實(shí)現(xiàn)它,動(dòng)作給發(fā)送到響應(yīng)鏈里胰锌,直到它到達(dá) shared application肌稻,然后應(yīng)用就推出了。
編譯運(yùn)行匕荸,點(diǎn)擊 status item爹谭,你會(huì)看見一個(gè)菜單。有進(jìn)步榛搔!
試一下這些選項(xiàng) —— 選擇 Print Quotes 會(huì)在 Xcode 控制臺(tái)里顯示名言诺凡,Quit Quotes 會(huì)退出 app。
給 Status Item 添加 Popover
可以看到践惑,用代碼設(shè)置一個(gè)菜單是這么簡(jiǎn)單腹泌,但在 Xcode 控制臺(tái)里顯示名言對(duì)于用戶來說并沒有什么卵用。下一步是替換菜單為一個(gè)簡(jiǎn)單的視圖控制器來顯示名言尔觉。
選擇 File/New/File…凉袱,選擇 OS X/Source/Cocoa Class 模板然后點(diǎn)擊 Next。
把類命名為 ** QuotesViewController侦铜,父類設(shè)置為 ** NSViewController专甩,勾上 Also create XIB file for user interface,設(shè)置語言為 Swift钉稍。
最后涤躲,再次點(diǎn)擊 Next,選擇一個(gè)地方來保存文件(項(xiàng)目文件夾里的 Quotes 子文件夾是一個(gè)好地方)然后點(diǎn)擊 Create贡未。
選擇种樱,把新文件放到一邊蒙袍,回到 AppDelegate.swift。給這個(gè)類添加一個(gè)新的 property 聲明:
let popover = NSPopover()
下一步嫩挤,替換 applicationDidFinishLaunching(_:) 為下面這段代碼:
func applicationDidFinishLaunching(notification: NSNotification) {
if let button = statusItem.button {
button.image = NSImage(named: "StatusBarButtonImage")
button.action = #selector(AppDelegate.togglePopover(sender:))
}
popover.contentViewController = QuotesViewController(nibName: "QuotesViewController", bundle: nil)
}
已經(jīng)把按鈕動(dòng)作改為 togglePopover:害幅,接下來就要實(shí)現(xiàn)它。還有岂昭,不是在設(shè)置一個(gè)菜單矫限,而是設(shè)置了 popover 來顯示 QuotesViewController 里所有東西。
最后佩抹,移除 printQuote()叼风,添加下面三個(gè)方法到原本的位置:
func showPopover(sender: AnyObject?) {
if let button = statusItem.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
func closePopover(sender: AnyObject?) {
popover.performClose(sender)
}
func togglePopover(sender: AnyObject?) {
if popover.isShown {
closePopover(sender: sender)
} else {
showPopover(sender: sender)
}
}
showPopover 為用戶顯示 popover。和 iOS 上的 popover 相似棍苹,你只需要提供一個(gè) source rect无宿,然后 OS X 就會(huì)放置 popover 和箭頭,這樣它看起來就是從菜單欄圖標(biāo)里出來的枢里。
closePopover() 很簡(jiǎn)單孽鸡,就是關(guān)閉 popover,togglePopover() 是 action 方法栏豺,基于當(dāng)前狀態(tài)來打開或關(guān)閉 popover彬碱。
編譯運(yùn)行,然后點(diǎn)擊菜單欄圖標(biāo)來檢查一下是否顯示和隱藏了一個(gè)空的 popover奥洼。
popover 工作起來很棒巷疼,但激發(fā)靈感的名人名言都去哪了?可以看到的只是一個(gè)空 view 也沒有名言灵奖。猜猜接下來要修復(fù)什么嚼沿?
實(shí)現(xiàn) Quote View Controller
首先,需要模型化來保存名言和作者瓷患。選擇 File/New/File… 然后選擇 OS X/Source/Swift File 模板骡尽,然后點(diǎn)擊 Next。把文件命名為 Quote 然后點(diǎn)擊 Create擅编。
打開 Quote.swift 然后把下面這段代碼加在文件里:
struct Quote {
let text: String
let author: String
static let all: [Quote] = [
Quote(text: "Never put off until tomorrow what you can do the day after tomorrow.", author: "Mark Twain"),
Quote(text: "Efficiency is doing better what is already being done.", author: "Peter Drucker"),
Quote(text: "To infinity and beyond!", author: "Buzz Lightyear"),
Quote(text: "May the Force be with you.", author: "Han Solo"),
Quote(text: "Simplicity is the ultimate sophistication", author: "Leonardo da Vinci"),
Quote(text: "It’s not just what it looks like and feels like. Design is how it works.", author: "Steve Jobs")
]
}
// MARK: - Printable
extension Quote: Printable {
var description: String {
return "\"\(text)\" — \(author)"
}
}
這樣就定義了一個(gè)簡(jiǎn)單的名言結(jié)構(gòu)體攀细,還有一個(gè)靜態(tài) property 可以返回所有的名言。因?yàn)橐沧?Quote 遵循 Printable 了爱态,就可以輕易得到一個(gè)格式優(yōu)雅的字符串碉钠。
又有進(jìn)展了局扶,但在 UI 里還需要更多函數(shù)丐枉。不如用一些 wrapping 和 auto constraint 來讓它更漂亮耕餐?
打開 QuotesViewController.xib 然后拖兩個(gè) bevel button 實(shí)例修壕,一個(gè) label 和 push button 到自定義視圖里瞬逊。
設(shè)置第一個(gè) bevel button 的圖片為 NSGoLeftTemplate哎媚,第二個(gè)按鈕的圖片設(shè)置為 NSGoRightTemplate占遥,設(shè)置 label 的文字 alignment 為 Center 以及 line break 模型設(shè)置為 Word Wrap。最后痘煤,設(shè)置 push button 的 title 為 Quit Quotes凑阶。
最后的布局應(yīng)該看起來像這樣:
你會(huì)添加 auto layout constraints 來讓用戶界面匹配嗎?在劇透前給一點(diǎn)好的暗示衷快。如果你會(huì)的話宙橱,跳過暗示然后給自己一朵小紅花。
解決方式
要獲得正確的布局蘸拔,需要添加如下 auto layout constraints:
- Pin go-left 和 go-right 按鈕的 top 和 bottom师郑,然后給它們固定的寬度 32。go-left 也應(yīng)該被固定到 leading 邊调窍,go-right 應(yīng)該被固定到 trailing 邊宝冕。
- 把 label 放到按鈕中間,然后添加 trailing 和 leading space constraints邓萨。還要把 label 設(shè)置為垂直居中地梨。
- 固定 Quit 按鈕到底邊,水平居中缔恳。
把 constraint 設(shè)置完美后宝剖,在畫布的右下角選擇 Resolve Auto Layout Issues 來選擇 Update Constraints。
現(xiàn)在打開 QuotesViewController.swift 然后用下面這段代碼替換文件內(nèi)容:
import Cocoa
class QuotesViewController: NSViewController {
@IBOutlet var textLabel: NSTextField!
}
// MARK: Actions
extension QuotesViewController {
@IBAction func goLeft(sender: NSButton) {
}
@IBAction func goRight(sender: NSButton) {
}
@IBAction func quit(sender: NSButton) {
}
}
這個(gè) starter implementation 就是一個(gè)標(biāo)準(zhǔn)的 NSViewController 實(shí)例歉甚。text label 有一個(gè) outlet万细,用來更新名言警句。三個(gè) action 是給三個(gè)按鈕準(zhǔn)備的纸泄。
然后回到 QuotesViewController.xib 然后把 outlet 連接到 text label雅镊,只要按住 control 從 File’s Owner 拖到 label 上。再按住 control 把按鈕拖到 File’s Owner 來連接對(duì)應(yīng)的 action刃滓。
注意:如果你對(duì)上面的步驟有什么困惑仁烹,參考我們的 OS X tutorials,這是介紹性教程咧虎,介紹了 OS X 開發(fā)的多個(gè)方面卓缰,包括在 interface builder 里添加 views/constraints 以及連接 outlets 和 actions。
站起來砰诵,伸個(gè)懶腰或者繞著辦公桌轉(zhuǎn)一圈征唬,因?yàn)槟銊倓偼瓿闪艘淮蠖?interface builder 工作。
編譯運(yùn)行茁彭,你的 popover 現(xiàn)在看起來就會(huì)像這樣:
注意:上面的 popover 使用了 view controller 的默認(rèn)尺寸总寒。如果你想要更少或更大的 popover,只需要在 xib 里改變 view controller 的大小即可理肺。試試看摄闸!
界面完成了善镰,但還沒有做完所有的工作。這些按鈕在等你通知它們年枕,當(dāng)用戶點(diǎn)擊的時(shí)候要做什么——不要把它們掛在這兒炫欺。
打開 QuotesViewController.swift 然后把下面的 property 添加到類里:
let quotes = Quote.all
var currentQuoteIndex: Int = 0 {
didSet {
updateQuote()
}
}
第一個(gè) property 管理所有的 quote,第二個(gè)管理當(dāng)前 quote 的索引熏兄。currentQuoteIndex 還有一個(gè) property observer 來更新 text label 字符串為新的名言品洛,就在每次 index 被改變的時(shí)候。
接下來摩桶,為類添加下面的方法:
override func viewWillAppear() {
super.viewWillAppear()
currentQuoteIndex = 0
}
func updateQuote() {
textLabel.stringValue = toString(quotes[currentQuoteIndex])
}
當(dāng) view 顯示的時(shí)候桥状,把當(dāng)前名言 index 設(shè)置為 0,然后就會(huì)更新用戶界面硝清。updateQuote() 只是更新 text label 來顯示當(dāng)前選中的是哪個(gè) quote辅斟,參考了 currentQuoteIndex。
要把它全部綁在一起耍缴,實(shí)現(xiàn)如下的三個(gè) action 方法:
@IBAction func goLeft(sender: NSButton) {
currentQuoteIndex = (currentQuoteIndex - 1 + quotes.count) % quotes.count
}
@IBAction func goRight(sender: NSButton) {
currentQuoteIndex = (currentQuoteIndex + 1) % quotes.count
}
@IBAction func quit(sender: NSButton) {
NSApplication.sharedApplication().terminate(sender)
}
在 goLeft() 和 goRight() 里砾肺,循環(huán)了所有的名言,如果到達(dá)數(shù)組的末尾就回頭防嗡。quit() 關(guān)閉了 app变汪,之前已經(jīng)解釋過了。
再次編譯運(yùn)行蚁趁,現(xiàn)在你可以看到所有的 quote 并且可以退出 app裙盾!
Event Monitoring
這個(gè)毫不起眼的小菜單欄 app 還需要一個(gè)功能,那就是當(dāng)你點(diǎn)擊到 app 之外的任意地方的時(shí)候他嫡,popover 會(huì)自動(dòng)關(guān)閉番官。
菜單欄 app 應(yīng)該在點(diǎn)擊或滑過的時(shí)候打開 popover,然后再用戶移到下一個(gè)東西的時(shí)候消失钢属。對(duì)于這點(diǎn)徘熔,我們需要一個(gè) OS X 全局 event monitor。
Here’s where you’ll take the concept to the next level. 要讓 event monitor 在所有項(xiàng)目里可復(fù)用淆党,還要保持示例 app 模塊化酷师,我們會(huì)定義一個(gè) Swift wrapper 類,然后在顯示 popover 的時(shí)候使用它染乌。
Bet you’re feeling smarter already!
創(chuàng)建一個(gè) Swift 文件山孔,命名為 EventMonitor,然后用如下類定義來替換內(nèi)容:
import Cocoa
public class EventMonitor {
private var monitor: AnyObject?
private let mask: NSEventMask
private let handler: NSEvent? -> ()
public init(mask: NSEventMask, handler: NSEvent? -> ()) {
self.mask = mask
self.handler = handler
}
deinit {
stop()
}
public func start() {
monitor = NSEvent.addGlobalMonitorForEventsMatchingMask(mask, handler: handler)
}
public func stop() {
if monitor != nil {
NSEvent.removeMonitor(monitor!)
monitor = nil
}
}
}
傳遞要監(jiān)聽的事件 mask 即可初始化這個(gè)類 —— 比如說按下某個(gè)鍵荷憋、滾輪滑動(dòng)台颠、單擊鼠標(biāo)左鍵,等等勒庄,還要另外傳遞一個(gè) event handler串前。
當(dāng)你準(zhǔn)備好開始監(jiān)聽的時(shí)候瘫里,start() 調(diào)用了 addGlobalMonitorForEventsMatchingMask(_:handler:),which returns an object for you to hold on to酪呻。每次 mask 中特定的事件發(fā)生后减宣,系統(tǒng)會(huì)調(diào)用你的 handler盐须。
要移除全局 event monitor 的話玩荠,在 stop() 里調(diào)用 removeMonitor() 然后通過設(shè)置返回的對(duì)象為 nil 來刪除它。
剩下需要做的就是在需要的時(shí)候調(diào)用 start() 和 stop()贼邓。多簡(jiǎn)單呀阶冈?這個(gè)類也為你在 deinitializer 中調(diào)用 stop(),來清理自己塑径。
最后一次打開 AppDelegate.swift女坑,為類添加一個(gè)新的 property 聲明:
var eventMonitor: EventMonitor?
接下來,在 applicationDidFinishLaunching(_:) 結(jié)尾的地方添加代碼來配置 event monitor:
eventMonitor = EventMonitor(mask: .LeftMouseDownMask | .RightMouseDownMask) { [unowned self] event in
if self.popover.shown {
self.closePopover(event)
}
}
eventMonitor?.start()
這樣就在系統(tǒng)檢測(cè)到任意左鍵或右鍵按下事件的時(shí)候通知你的 app统舀,然后關(guān)閉 popover匆骗。注意你的 handler 不會(huì)被發(fā)送到自己的應(yīng)用事件被調(diào)用。所以 popover 當(dāng)你點(diǎn)擊內(nèi)部的時(shí)候不會(huì)關(guān)閉 popover誉简。:]
添加如下代碼到 showPopover(_:) 的結(jié)尾處:
eventMonitor?.start()
這會(huì)在 popover 顯示的時(shí)候啟動(dòng) event monitor碉就。
然后,需要把下面的代碼添加到 closePopover(_:) 的結(jié)尾:
eventMonitor?.stop()
這會(huì)在 popover 關(guān)閉的時(shí)候停止 event monitor闷串。
全部完成了瓮钥!再一次編譯運(yùn)行 app。點(diǎn)擊菜單欄圖標(biāo)來顯示 popover烹吵,然后點(diǎn)擊任意其他位置碉熄,popover 就會(huì)關(guān)閉±甙危酷炫锈津!
下面看什么?
可以在這里下載最終項(xiàng)目凉蜂,帶有你在上面的教程里開發(fā)的所有代碼琼梆。
你已經(jīng)看到如何在菜單欄 status item 里設(shè)置菜單和 popover —— 為什么不繼續(xù)實(shí)驗(yàn)顯示隨機(jī)的名言,連接到 web 后端來獲取新的名言跃惫,甚至提供一個(gè)“pro”版來賺點(diǎn)錢呢叮叹?
尋找其它機(jī)會(huì)的好地方是閱讀 [NSMenu]( NSMenu - AppKit | Apple Developer Documentation ),[NSPopover]( NSPopover - AppKit | Apple Developer Documentation ) 和 [NSStatusItem]( NSStatusItem - AppKit | Apple Developer Documentation ) 的官方文檔爆存。
專業(yè)提示:小心 NSStatusItem 文檔蛉顽。API 在 Yosemite 里發(fā)生了重大變更,不幸的是先较,文檔把所有舊的方法都標(biāo)記為 deprecated携冤,但沒有記載新的替換方法悼粮。對(duì)于這點(diǎn),你需要在 Xcode 里按住 command 點(diǎn)擊 NSStatusItem 來查看生成的 Swift 頭文件≡兀現(xiàn)在只有少數(shù)幾個(gè)方法了扣猫,所有功能都在一個(gè) NSButton 里,所以理解起來相當(dāng)?shù)妮p松翘地。
感謝花時(shí)間學(xué)習(xí)如何制作一個(gè)酷酷的 OS X popover 菜單 app∩暧龋現(xiàn)在看是相當(dāng)簡(jiǎn)單了,但你可以看到在這里學(xué)習(xí)的概念對(duì)于許多種 app 都是絕佳的基礎(chǔ)衙耕。
如果你在 app 里配置 status item昧穿、菜單或 popover 的時(shí)候有任何疑問,新奇的發(fā)現(xiàn)或想法橙喘,想告訴其他人时鸵,可以在下面評(píng)論來告訴我!:]