macOS应用开发基础之Popover mac中安装了一个叫 Pap.er 的app,用来下载和管理桌面壁纸,壁纸资源质量很高幺,app不但很轻量(仅有5MB)而且设计精美,还免费的(我这算在帮他们做推广吗^_^,好的东西就应该推广一下)。这款app采用了状态栏小工具的形式,界面在 Popover 中实现。看视频感受一下,叫Popover的弹窗,就是本文要讲的东西。Pap.er - 专为 Mac 设计的壁纸应用?paper.meiyuan.in00: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,可以看到界面设计中有Window 和 MainMenu,两个选中删掉; 打开文件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 36px 和 54px 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的介面指定给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,再次点击就会关闭。 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 就可以实现理想结果,不用操作弹窗中的内容,在弹窗外点击就会收起弹窗! 显然上面的配置也不是我们想要的,网上看到一种神奇的方式:添加系统事件监视器来实现对交互事件的监测,从而做到弹窗显示后,无论什么时候点击弹窗外面都能收起弹窗的效果。 新建名为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。运行程序,当啷啷,符合预期! 6. 运行效果 最终运行效果:00:07Popover 实例最终效果完整的工程可以点击下面卡片下载:点我?pichome-1254392422.cos.ap-chengdu.myqcloud.com 参考: Menus and Popovers in Menu Bar Apps for macOS?www.raywenderlich.comAppKit | Apple Developer Documentation?developer.apple.comAppKit | Apple Developer Documentation?developer.apple.com 博客原文:macOS应用开发基础之Popover?www.smslit.top 推荐阅读: 相关文章 {{#data}} {{title}} {{/data}}