macOS应用开发基础之Popover
mac中安装了一个叫pap.er的app,用来下载和管理桌面壁纸,壁纸资源质量很高幺,app不但很轻量(仅有5MB)而且设计精美,还免费的(我这算在帮他们做推广吗^_^,好的东西就应该推广一下)。这款app采用了状态栏小工具的形式,界面在 Popover 中实现。看视频感受一下,叫Popover的弹窗,这是本文要讲的东西。
昨天研究了Popover的使用,所以今天以一个实例与大家分享一下!
平台
- macOS 10.13.5
- Xcode 9.4.1
- swift 4.1.2
本文基于上述平台实现,下面的代码中可能随着Swift语言的版本的不同会需要调整(不过应该不多),xcode会比较智能的提出修改建议,视情况调整即可,不过实现思路是一致的。
新建及配置工程
-
打开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! -
再次运行程序,主窗口也不显示了,连菜单栏也木有了,不要着急,咱继续。
添加状态栏按钮
打开文件AppDelegate.swift,在类中添加属性,这一步是创建一个状态栏按钮,设置宽度属性NSStatusItem.squareLength,代码如下:
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
状态栏按钮总该需要一个图标吧!打开Assets.xcassets,右击显示AppIcon下方的空白区,选择New Image Set,重命名为statusIcon,当然这个名字随便定,选中这个图集,会看到右侧有配置区,配置图集按照Template Image渲染:

看到有三个虚线框空白区,这就是图片区,状态栏按钮的图片基本大小为 ,还需2倍和3倍的适用于视网膜屏幕的mac,像素分别是 和 ,可以使用以下我提供的图标:



分别将图拖到对应位置:

切换到文件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还有个暗黑主题不是?
两种主题下的效果如下:


添加Popover
添加Popover控件
打开文件MainMenu.xib,右下脚搜索控件Popover就会看到:

点击控件将其拖入界面,添加后其并没有可视化的元素,可以在Objects管理器中看到已经添加成功:

启动Assitant Editor,按住Contorl键点击Popover拖入AppDelegate.swift文件,创建popover属性:
添加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,再次点击就会关闭。
设计Popover界面
上面提到,我们可以在PopoverDemoViewController.xib中设计popover的界面。
添加应用退出按钮
-
打开
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,此时可以看到刚才添加的按钮,点击一下按钮,程序就会退出。
添加无用的标签
总要在程序中显示点东西吧,就像添加按钮一样拖动一个Label控件到view中,内容改为经典的Hello, World!,啊,不行太俗了,还是改为Hello, Popover!吧,然后调整标签大小,并调整位置在水平和垂直居中的位置,调整内容居中:

运行程序,看到想要的效果!
优化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。运行程序,当啷啷,符合预期!
运行效果:
最终的这么简陋的程序的运行效果:
完整的工程可以点我下载。
v1.5.2