菜单栏上的应用程序早已为macOS用户所熟知。 其中一些应用程序具有“正常”部分,其他应用程序仅位于菜单栏中。
在本指南中,您将编写一个应用程序,该应用程序在弹出窗口中显示名人的一些报价。 在创建此应用程序的过程中,您将学到:
- 在菜单栏中分配应用程序图标
- 使应用程序仅托管在菜单栏上
- 添加自定义菜单
- 根据用户的要求显示一个弹出窗口,并在必要时使用事件监视将其隐藏
注意:本指南假定您熟悉Swift和macOS。
开始使用
启动Xcode。 接下来,在
File / New / Project ...菜单上,选择
macOS / Application / Cocoa App模板,然后单击
Next 。
在下一个屏幕上,输入Quotes作为
产品名称 ,选择您的
组织名称和
组织标识符 。 然后,确保已选择Swift作为应用程序语言,并选中了
使用Storyboards复选框。 取消选中“
创建基于文档的应用程序” ,“
使用核心数据” ,“
包含单元测试”和“
包括UI测试”复选框 。

最后,再次单击
下一步 ,指定保存项目的位置,然后单击
创建 。
创建新项目后,打开
AppDelegate.swift并将以下属性添加到类中:
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
在这里,我们在菜单栏中创建状态项(应用程序图标),固定长度对用户可见。
然后,我们需要将图片分配给菜单栏中的该新项目,以便区分新应用程序。
在项目导航器中,转到Assets.xcassets,
上传图片并将其拖动到资产目录中。
选择图片并打开属性检查器。 将“
渲染为”选项更改为“
模板图像” 。

如果您使用自己的图像,请确保该图像是黑白图像,并将其配置为“
模板”图像,以使该图标在深色和浅色菜单栏上看起来都很好。
返回
AppDelegate.swift ,并将以下代码添加到
applicationDidFinishLaunching(_ :) if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(printQuote(_:)) }
在这里,我们将刚刚添加的应用程序图标分配给应用程序图标,并在单击它时分配
操作 。
将以下方法添加到该类:
@objc func printQuote(_ sender: Any?) { let quoteText = "Never put off until tomorrow what you can do the day after tomorrow." let quoteAuthor = "Mark Twain" print("\(quoteText) — \(quoteAuthor)") }
此方法只是将报价单打印到控制台。
注意
objc方法
指令 。 这使您可以使用此方法作为对按钮单击的响应。
生成并运行该应用程序,您将在菜单栏中看到新的应用程序。 万岁!
每次单击菜单栏中的图标时,Xcode控制台中都会显示著名的Mark Twain谚语。
我们将主窗口和图标隐藏在扩展坞中
在直接处理功能之前,我们需要做一些小事情:
要删除停靠图标,请打开
Info.plist 。 添加一个新的
Application is agent(UIElement)键,并将其值设置为
YES 。

现在是时候处理主应用程序窗口了。
- 打开Main.storyboard
- 选择“ 窗口控制器”场景并将其删除
- 查看Controller场景假,我们将尽快使用它

生成并运行该应用程序。 现在,应用程序在扩展坞中既没有主窗口又没有不必要的图标。 太好了!
将菜单添加到状态项
对于认真的应用程序,单击响应显然是不够的。 添加功能的最简单方法是添加菜单。 在
AppDelegate的末尾添加此功能。
func constructMenu() { let menu = NSMenu() menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P")) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) statusItem.menu = menu }
然后在
applicationDidFinishLaunching(_ :)的末尾添加此调用
constructMenu()
我们创建
NSMenu ,向其中添加3个
NSMenuItem实例,并将此菜单设置为应用程序图标菜单。
一些要点:
- 菜单项的标题是出现在菜单中的文本。 一个本地化应用程序的好地方(如有必要)。
- 动作 (如按钮或其他控件的动作)是用户单击菜单项时调用的方法
- keyEquivalent是键盘快捷键,可用于选择菜单项。 小写字符使用Cmd作为修饰符,小写字符使用Cmd + Shift 。 仅在应用程序位于最顶部且处于活动状态时,此方法才有效。 在我们的情况下,必须显示菜单或其他窗口,因为我们的应用程序在扩展坞中没有图标
- spacerItem是处于非活动状态的菜单项,其形式为其他元素之间的灰线。 用它来分组
- printQuote是您已经在AppDelegate中定义的方法,而终止是NSApplication定义的方法。
启动应用程序,然后单击应用程序图标将看到一个菜单。

尝试单击菜单-选择“
打印报价”会在Xcode控制台中显示报价,而“
退出报价”将终止应用程序。
添加一个弹出窗口
您已经看到从代码中添加菜单是多么容易,但是在Xcode控制台中显示引号显然不是用户对应用程序的期望。 现在,我们将添加一个简单的视图控制器,以正确的方式显示引号。
转到
File / New / File ...菜单,选择
macOS / Source / Cocoa Class模板,然后单击
Next 。

- 将类命名为QuotesViewController
- 成为NSViewController的继承者
- 确保未选中“ 同时为用户界面创建XIB文件”复选框
- 将语言设置为Swift
最后,再次单击“
下一步” ,选择一个保存文件的位置,然后单击“
创建” 。
现在打开
Main.storyboard 。 展开
View Controller Scene,然后选择
View Controller实例 。

首先选择
Identity Inspector并将类更改为
QuotesViewController ,然后将
Storyboard ID设置为
QuotesViewController现在,将以下代码添加到
QuotesViewController.swift文件的末尾:
extension QuotesViewController {
这是怎么回事:
- 我们有一个指向Main.storyboard的链接。
- 创建一个与我们刚在上方安装的场景标识符匹配的场景标识符 。
- 创建QuotesViewController的实例并返回它。
您创建此方法后,现在使用
QuotesViewController的每个人都不需要知道如何创建它。 它只是工作。
注意
guard语句中的
fatalError 。 最好使用它或
assert assertFailure,以便在开发中出现问题时,您自己和开发团队的其他成员都可以知道。
现在回到
AppDelegate.swift 。 添加一个新的属性。
let popover = NSPopover()
然后,将
pplicationDidFinishLaunching(_ :)替换为以下代码:
func applicationDidFinishLaunching(_ aNotification: Notification) { if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(togglePopover(_:)) } popover.contentViewController = QuotesViewController.freshController() }
您已更改click操作,以调用
togglePopover(_ :)方法,稍后我们将进行编写。 另外,我们没有配置和添加菜单,而是配置了一个弹出窗口,该弹出窗口将显示
QuotesViewController中的内容 。
将以下三种方法添加到
AppDelegate :
@objc func togglePopover(_ sender: Any?) { if popover.isShown { closePopover(sender: sender) } else { showPopover(sender: sender) } } func showPopover(sender: Any?) { if let button = statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) } } func closePopover(sender: Any?) { popover.performClose(sender) }
showPopover()显示一个弹出窗口。 您只需指出它的来源,macOS便将其定位并绘制一个箭头,就像它从菜单栏中出现一样。
closePopover()仅关闭弹出窗口,而
togglePopover()是根据其状态显示或隐藏弹出窗口的方法。
启动应用程序,然后单击其图标。

一切都很好,但是内容在哪里?
我们实现Quote View Controller
首先,您需要一个用于存储引号和属性的模型。 转到
File / New / File ...菜单,然后选择
macOS / Source / Swift File模板 ,然后选择
Next 。 将文件命名为
Quote ,然后单击
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") ] } extension Quote: CustomStringConvertible { var description: String { return "\"\(text)\" — \(author)" } }
在这里,我们定义了一个简单的报价结构和一个返回所有报价的静态属性。 由于我们使
Quote符合
CustomStringConvertible协议,因此我们可以轻松地获取格式方便的文本。
有进步,但是我们仍然需要控件来显示所有这一切。
添加界面元素
打开Main.storyboard,然后拉出视图控制器上的3个按钮(
Push Button )和标签(
Multiline Label) 。
放置按钮和标签,使它们看起来像这样:

将左按钮以20的间距连接到左边缘,并垂直居中。
将右按钮以20的间距连接到右边缘,并垂直居中。
将底部按钮以20的间距连接到底部边缘,并水平居中。
将标记的左右边缘与按钮之间的间距为20,垂直居中。

您将看到几个布局错误,因为没有足够的信息可以
自动布局解决它。
将“
水平内容拥抱优先级”设置为249,以允许调整标签的大小。

现在执行以下操作:
- 将左按钮图像设置为NSGoLeftTemplate并清除标题
- 将右按钮图像设置为NSGoRightTemplate并清除标题
- 将下面按钮的标题设置为“ 退出报价” 。
- 将标签的文本对齐方式设置为居中。
- 检查标签上的“换行符”是否设置为“自动换行” 。
现在打开
QuotesViewController.swift并将以下代码添加到
QuotesViewController类的实现中:
@IBOutlet var textLabel: NSTextField!
将此扩展添加到类实现中。 现在在
QuotesViewController.swift中有两个类扩展。
我们仅添加了用于显示引号的标签的
插座 ,以及将与按钮连接的3个存根方法。
将代码与Interface Builder连接
注意:Xcode在您的代码左侧
-IBAction和
IBOutlet关键字旁边放置了圆圈。

我们将使用它们将代码连接到UI。
按住
Alt键的同时,在
项目导航器中单击
Main.storyboard 。 因此,
情节提要板在右侧的
助手编辑器中打开,在左侧的代码中打开。
将
textLabel左侧的
圆圈拖到
界面构建器中的标签上。 以相同的方式,分别将上
一个 ,
下一个和
退出方法与左,右和下按钮组合。

启动您的应用程序。

我们使用了默认的弹出窗口大小。 如果您想要更大或更小的弹出窗口,只需在
情节提要中调整其大小即可。
为按钮编写代码
如果您尚未隐藏
助手编辑器 ,请单击
Cmd-Return或V
iew>标准编辑器>显示标准编辑器打开
QuotesViewController.swift并将以下属性添加到类实现中:
let quotes = Quote.all var currentQuoteIndex: Int = 0 { didSet { updateQuote() } }
quotes属性包含所有引号,
currentQuoteIndex是当前显示的引号的索引。
CurrentQuoteIndex还具有一个
属性观察器,以在索引更改时用新的引号更新标签的内容。
现在添加以下方法:
override func viewDidLoad() { super.viewDidLoad() currentQuoteIndex = 0 } func updateQuote() { textLabel.stringValue = String(describing: quotes[currentQuoteIndex]) }
当视图加载时,我们将引号索引设置为0,这又导致对接口的更新。
updateQuote()只是更新文本标签以显示报价。 对应的
currentQuoteIndex 。
最后,使用以下代码更新这些方法:
@IBAction func previous(_ sender: NSButton) { currentQuoteIndex = (currentQuoteIndex - 1 + quotes.count) % quotes.count } @IBAction func next(_ sender: NSButton) { currentQuoteIndex = (currentQuoteIndex + 1) % quotes.count } @IBAction func quit(_ sender: NSButton) { NSApplication.shared.terminate(sender) }
next()和
previous()方法在所有引用中循环。
退出关闭应用程序。
启动应用程序:

事件监控
用户还希望我们的应用程序做一件事-当用户单击窗口外的某个地方时,隐藏弹出窗口。 为此,我们需要一种名为
macOS全局事件监视器的机制。
创建一个新的Swift文件,将其命名为
EventMonitor ,并将其内容替换为以下代码:
import Cocoa public class EventMonitor { private var monitor: Any? private let mask: NSEvent.EventTypeMask private let handler: (NSEvent?) -> Void public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { self.mask = mask self.handler = handler } deinit { stop() } public func start() { monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) } public func stop() { if monitor != nil { NSEvent.removeMonitor(monitor!) monitor = nil } } }
初始化此类的实例时,我们将为它传递一个将要监听的事件掩码(例如击键,鼠标滚轮等)和一个事件处理程序。
当我们准备开始监听时,
start()调用
addGlobalMonitorForEventsMatchingMask(_:handler :) ,它返回我们正在保存的对象。 一旦掩码中包含的事件发生,系统就会调用您的处理程序。
要停止监视事件,请在
stop()中调用
removeMonitor () ,然后通过
将其
设置为nil来删除该对象。
我们剩下的就是在正确的时间调用
start()和
stop() 。 该类还会在反初始化程序上调用
stop()进行清理。
连接事件监视器
上一次打开
AppDelegate.swift并添加一个新属性:
var eventMonitor: EventMonitor?
然后在
applicationDidFinishLaunching(_ :)的末尾添加此代码以配置
事件监视器 。
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let strongSelf = self, strongSelf.popover.isShown { strongSelf.closePopover(sender: event) } }
当您单击向左或向右按钮时,这将通知您的应用程序。 请注意:处理程序将不会响应您在应用程序内部的鼠标单击而被调用。 这就是为什么当您在弹出窗口中单击时弹出窗口不会关闭的原因。
我们对
自身使用
弱引用,以避免在
AppDelegate和
EventMonitor之间形成强链接循环的危险。
在
showPopover(_ :)方法的末尾添加以下代码:
eventMonitor?.start()
当弹出窗口出现时,我们开始监视事件。
现在,将代码添加到
closePopover(_ :)方法的末尾:
eventMonitor?.stop()
当弹出窗口关闭时,我们在这里结束监视。
该应用程序已准备就绪!
结论
在这里,您将找到该项目的完整代码。
您已经了解了如何在菜单栏上的应用程序中设置菜单和弹出窗口。 为什么不尝试使用多个标签或带格式的文本来更好地查看引号? 还是连接后端以接收来自Internet的报价? 还是要使用键盘在引号之间导航?
研究的好地方是官方文档:
NSMenu ,
NSPopover和
NSStatusItem 。