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
渲染:
看到有三个虚线框空白区,这就是图片区,状态栏按钮的图片基本大小为 $18px\times18px$ ,还需2倍和3倍的适用于视网膜屏幕的mac,像素分别是 $36px\times36px$ 和 $54px\times54px$ ,可以使用以下我提供的图标:
分别将图拖到对应位置:
切换到文件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
。运行程序,当啷啷,符合预期!
运行效果:
最终的这么简陋的程序的运行效果:
完整的工程可以点我下载。