mac中安装了一个叫 Pap.er 的app,用来下载和管理桌面壁纸,壁纸资源质量很高幺,app不但很轻量(仅有5MB)而且设计精美,还免费的(我这算在帮他们做推广吗^_^,好的东西就应该推广一下)。这款app采用了状态栏小工具的形式,界面在 Popover 中实现。看视频感受一下,叫Popover的弹窗,就是本文要讲的东西。
今天以一个简单的实例分享一下如何实现 Popover 弹窗。
本文基于上述平台实现,下面的代码中可能随著 Swift 语言的版本更新会需要调整(不过应该不多),Xcode n会比较智能的提出修改建议,视情况调整即可,不过实现思路是一致的。
PopoverDemo
Swift
Storyboard
Info
Custom macOS application Target Properties
Application is agent(UI Element)
MainMenu.xib
AppDelegate.swift
@IBOutlet weak var window: NSWindow!
打开文件AppDelegate.swift,在类中添加属性,这一步是创建一个状态栏按钮,设置宽度属性NSStatusItem.squareLength,代码如下:
NSStatusItem.squareLength
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
状态栏按钮总该需要一个图标吧!打开Assets.xcassets,右击显示AppIcon下方的空白区,选择New Image Set,重命名为statusIcon,当然这个名字随便定,选中这个图集,会看到右侧有配置区,配置图集按照Template Image渲染:
Assets.xcassets
AppIcon
New Image Set
statusIcon
Template Image
看到有三个虚线框空白区,这就是图片区,状态栏按钮的图片基本大小为 18px x 18px ,还需2倍和3倍的适用于视网膜屏幕的 mac,像素分别是 36px x 36px 和 54px x 54px ,可以使用以下我提供的图标:
分别将图拖到对应位置:
切换到文件AppDelegate.swift,定义一个测试状态栏按钮点击行为的函数,这里以关闭应用程序为例吧,实现的函数:
@objc func quitApp(_ sender: AnyObject) { NSApplication.shared.terminate(self) }
然后找到applicationDidFinishLaunching在其中添加以下代码,为状态栏按钮配置图标和行为:
applicationDidFinishLaunching
if let button = statusItem.button { button.image = NSImage(named: NSImage.Name("statusIcon")) button.action = #selector(quitApp) }
此时运行程序会看到状态栏中出现了我们定义的按钮,点击一下,应用程序就退出了。
前面设置图片集渲染方式为Template Image,是为了适配不同的状态栏主题,因为macOS还有个暗黑主题不是?
两种主题下的效果如下:
打开文件MainMenu.xib,右下脚搜索控制项Popover就会看到:
Popover
点击控制项将其拖入界面,添加后其并没有可视化的元素,可以在Objects管理器中看到已经添加成功:
Objects
启动Assitant Editor,按住Contorl键点击Popover拖入AppDelegate.swift文件,创建popover属性:
Assitant Editor
Contorl
popover
00:10拖拽创建属性
此时popover是没有界面的,因为此时还没有为其指定view controller。
view controller
Command
n
PopoverDemoViewController
NSViewController
also create XIB file for user interface
Popover View Controller
PopoverDemoViewController.xib
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) } }
togglePopover
quitApp
if let button = statusItem.button { button.image = NSImage(named: NSImage.Name("statusIcon")) button.action = #selector(togglePopover) }
上面提到,我们可以在PopoverDemoViewController.xib中设计popover的界面。
Control
PopoverDemoViewContoller.swift
quiApp
@IBAction func quitApp(_ sender: Any) { NSApplication.shared.terminate(self) }
总要在程序中显示点东西吧,就像添加按钮一样拖动一个Label控制项到view中,内容改为经典的Hello, World!,啊,不行太俗了,还是改为Hello, Popover!吧,然后调整标签大小,并调整位置在水平和垂直居中的位置,调整内容居中:
Label
view
Hello, World!
Hello, Popover!
运行程序,看到想要的效果!
此时运行,你会发现有一个问题:点击弹窗外面,弹窗不会自动收起。这并不是我们想要的,查看apple官方的NSPopover文档,我们知道他有一个behavior属性,其值为NSPopover.Behavior.transient的时候好像可以实现,尝试一下。
NSPopover
behavior
NSPopover.Behavior.transient
打开MainMenu.xib,选中Popover,在其属性设置区就会看到Behavior,我们选择Transient,运行程序会发现:确实可以实现,点击弹窗外面,弹窗会自动收起,但是前提是必须在弹窗内有一次点击事件后才能做到这个效果。
Behavior
Transient
后来发现若弹窗内有 firs responder 就可以实现理想结果,不用操作弹窗中的内容,在弹窗外点击就会收起弹窗!
显然上面的配置也不是我们想要的,网上看到一种神奇的方式:添加系统事件监视器来实现对交互事件的监测,从而做到弹窗显示后,无论什么时候点击弹窗外面都能收起弹窗的效果。
EventMonitor
swift
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类,添加了构造函数和两个介面用于,创建用户操作事件监视器、启动和关闭监视器。
var eventMonitor: EventMonitor?
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let strongSelf = self, strongSelf.popover.isShown { strongSelf.closePopover(event!) } }
@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() }
Applicationed Defined
最终运行效果:
完整的工程可以点击下面卡片下载:
Menus and Popovers in Menu Bar Apps for macOS?www.raywenderlich.comAppKit | Apple Developer Documentation?developer.apple.comAppKit | Apple Developer Documentation?developer.apple.com
博客原文:
推荐阅读: