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: 20180629153024028025921.png

  • 输入工程名称:PopoverDemo,语言选择Swift,勾掉Storyboard: 20180629153024043769527.png

  • Next,点击 create 即可打开创建的新工程;

  • 点击运行按钮,可以看到程序运行,出现一个空的窗口,同时dock上出现了应用图标,这不是我们想要的,设置一下不显示它们:

    • 工程导航栏选中工程PopoverDemo,打开Info标签页;
    • 可以看到Custom macOS application Target Properties组,添加新的配置Application is agent(UI Element),布尔属性,值为 YES: 20180629153024095465288.png
  • 重新运行程序,可以看到已经不显示Dock图标;

  • 打开文件MainMenu.xib,可以看到界面设计中有WindowMainMenu,两个选中删掉;

  • 打开文件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渲染:

20180629153024293742991.png

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

分别将图拖到对应位置:

20180629153024408859417.png

切换到文件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还有个暗黑主题不是?

两种主题下的效果如下:

2018062915302450436095.png

20180629153024505287040.png

添加Popover

添加Popover控件

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

20180629153024689568360.png

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

20180629153024707619315.png

启动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会创建两个文件。

    20180629153024886162075.png

    • PopoverDemoViewController.swift
    • PopoverDemoViewController.xib
  • 打开文件MainMenu.xib,选择界面设计中的Popover View Controller,然后设置其对应的类为刚才创建的PopoverDemoViewController,此时popover的界面就可以在PopoverDemoViewController.xib中设计了:

    20180629153024927617269.png

  • 此时虽指定了具体的 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,再次点击就会关闭。

设计Popover界面

上面提到,我们可以在PopoverDemoViewController.xib中设计popover的界面。

添加应用退出按钮

  • 打开PopoverDemoViewController.xib文件,会看到一个view控件,我们拖动一个Push Button控件到view中,放置到右上角。

  • 选中添加的按钮,在控件属性窗口中, 取消勾选 Boarded,选择ImageNSStopProcessFrestandingTemplate

    20180629153025204615191.png

  • 调出Assitant Editor,按住Control键,点击按钮拖入PopoverDemoViewContoller.swift,创建action为点击事件quitApp,实现代码与之前的测试函数quiApp一样:

    @IBAction func quitApp(_ sender: Any) {
        NSApplication.shared.terminate(self)
    }
    
  • 运行程序,点击弹出popover,此时可以看到刚才添加的按钮,点击一下按钮,程序就会退出。

添加无用的标签

总要在程序中显示点东西吧,就像添加按钮一样拖动一个Label控件到view中,内容改为经典的Hello, World!,啊,不行太俗了,还是改为Hello, Popover!吧,然后调整标签大小,并调整位置在水平和垂直居中的位置,调整内容居中:

20180629153025294914532.png

运行程序,看到想要的效果!

优化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。运行程序,当啷啷,符合预期!

运行效果:

最终的这么简陋的程序的运行效果:

完整的工程可以点我下载。

参考: