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
图标

推荐阅读:

相关文章