SwiftUI 入门 — 系统状态栏程序

基于 Xcode Version 11.5

1. 新建项目

输入项目名称,选择 SwiftUI,选择 Core Data。

因为用到了 Core Data,这里需要修改自动生成的 AppDelegate.swift 中的一行 BUG 语句(升级到 Xcode 11.6 了依然存在。。)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// let contentView = ContentView().environment(\.managedObjectContext, persistentContainer.viewContext)
// 注释掉上面这句,修改为如下两句 👇
let context = persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
// let contentView = ContentView().environment(\.managedObjectContext, persistentContainer.viewContext) // 注释掉上面这句,修改为如下两句 👇 let context = persistentContainer.viewContext let contentView = ContentView().environment(\.managedObjectContext, context)
// let contentView = ContentView().environment(\.managedObjectContext, persistentContainer.viewContext)
// 注释掉上面这句,修改为如下两句 👇

let context = persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)

2. 在 ContentView 新增“退出”按钮

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button("Quit"){
NSApplication.shared.terminate(self)
}
}
}
}
struct ContentView: View { var body: some View { VStack { Text("Hello, World!") .frame(maxWidth: .infinity, maxHeight: .infinity) Button("Quit"){ NSApplication.shared.terminate(self) } } } }
struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            Button("Quit"){
                NSApplication.shared.terminate(self)
            }
        }
    }
}

运行该程序会显示如下窗口,点击 Quit 按钮可以直接退出程序。

3. 将程序图标追加到系统状态栏

3.1 新增图标

点击项目面板的 Assets.xcassets 图标,然后在编辑面板的左边点击右键,选择 “New Image Set”。

在新建的 Image Set 的 Attributes inspector 面板中,修改图片集的名字与可选尺寸。然后将一个 png 图标拖动到 image 面板的虚线框。

3.2 添加到系统状态栏 NSStatusBar

在项目根目录下新建一个 StatusBarController.swift 文件:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import AppKit
class StatusBarController {
private var statusItem: NSStatusItem
private var statusBarButton: NSStatusBarButton
init() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusBarButton = statusItem.button!
// 设置状态栏图标与标题
statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon")
statusBarButton.image!.size = NSSize(width: 18, height: 18)
statusBarButton.imagePosition = .imageLeft
statusBarButton.title = "倒计时"
// 设置状态栏图标点击事件
// #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针”
statusBarButton.action = #selector(statusBarButtonClicked(_:))
statusBarButton.target = self
}
// Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用
@objc func statusBarButtonClicked(_ sender: Any?) {
print("status bar button clicked")
}
}
import AppKit class StatusBarController { private var statusItem: NSStatusItem private var statusBarButton: NSStatusBarButton init() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusBarButton = statusItem.button! // 设置状态栏图标与标题 statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon") statusBarButton.image!.size = NSSize(width: 18, height: 18) statusBarButton.imagePosition = .imageLeft statusBarButton.title = "倒计时" // 设置状态栏图标点击事件 // #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针” statusBarButton.action = #selector(statusBarButtonClicked(_:)) statusBarButton.target = self } // Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用 @objc func statusBarButtonClicked(_ sender: Any?) { print("status bar button clicked") } }
import AppKit

class StatusBarController {
    private var statusItem: NSStatusItem
    private var statusBarButton: NSStatusBarButton
    
    init() {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        statusBarButton = statusItem.button!

        // 设置状态栏图标与标题
        statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon")
        statusBarButton.image!.size = NSSize(width: 18, height: 18)
        statusBarButton.imagePosition = .imageLeft
        statusBarButton.title = "倒计时"
        
        // 设置状态栏图标点击事件
        // #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针”
        statusBarButton.action = #selector(statusBarButtonClicked(_:))
        statusBarButton.target = self
    }
    
    // Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用
    @objc func statusBarButtonClicked(_ sender: Any?) {
        print("status bar button clicked")
    }
}

然后在 AppDelegate 类中添加存储属性:let statusBar = StatusBarController()

编译运行程序,在系统状态栏就会多出一个程序图标,点击图标即会在 Xcode 的控制台打印 “status bar button clicked”。

4. 隐藏 Dock 图标与关闭程序主窗口

4.1 隐藏 Dock 图标

在 Xcode 中打开 Info.plist,右键点击“ Add Row”。新建一行配置:Application is agent (UIElement) 并设置值为 YES。然后重新编译运行,就会发现 Dock 栏不再出现该程序图标。

4.2 关闭主窗口

打开 AppDelegate.swift,将默认的生成窗口被设置主视图的那段代码删除即可:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 删除下面这段代码
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
// 删除下面这段代码 window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false) window.center() window.setFrameAutosaveName("Main Window") window.contentView = NSHostingView(rootView: contentView) window.makeKeyAndOrderFront(nil)
// 删除下面这段代码
window = NSWindow(
    contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
    styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
    backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)

这时候编译运行会有一个 Warning 提示 let contentView 常量定义了却没有被使用。

5. 显示弹出视图

此时我们运行程序,什么都没有只有一个状态栏图标。我们想要的是点击这个状态栏图标能弹出一个程序视图,为此,我们需要生成一个 NSPopover 实例,并将 ContentView 绑定到该 NSPopover 实例上。

5.1 弹出框 NSPopover

首先,新建一个 ContentViewController.swift 文件:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import AppKit
class ContentViewController: NSViewController
{
// 内容为空即可
}
import AppKit class ContentViewController: NSViewController { // 内容为空即可 }
import AppKit

class ContentViewController: NSViewController
{
    // 内容为空即可
}

然后修改 StatusBarController 类,给他新增一个 popover 属性以及一些显示弹出窗口的方法:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import AppKit
class StatusBarController {
private var statusItem: NSStatusItem
private var statusBarButton: NSStatusBarButton
private var popover: NSPopover
init(_ popover: NSPopover) {
statusItem = NSStatusBar.system.statusItem(withLength: 28)
statusBarButton = statusItem.button!
self.popover = popover
statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon")
statusBarButton.image!.size = NSSize(width: 18, height: 18)
// #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针”
statusBarButton.action = #selector(togglePopover(_:))
statusBarButton.target = self
}
// Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用
@objc func togglePopover(_ sender: AnyObject)
{
print("status bar button clicked")
if popover.isShown {
hidePopover(sender)
} else {
showPopover(sender)
}
}
func showPopover(_ sender: AnyObject)
{
// relativeTo 参数表示 popover 关联视图的边界
// of 参数表示 popover 要关联的视图
// preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现
popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
func hidePopover(_ sender: AnyObject)
{
popover.performClose(sender)
}
}
import AppKit class StatusBarController { private var statusItem: NSStatusItem private var statusBarButton: NSStatusBarButton private var popover: NSPopover init(_ popover: NSPopover) { statusItem = NSStatusBar.system.statusItem(withLength: 28) statusBarButton = statusItem.button! self.popover = popover statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon") statusBarButton.image!.size = NSSize(width: 18, height: 18) // #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针” statusBarButton.action = #selector(togglePopover(_:)) statusBarButton.target = self } // Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用 @objc func togglePopover(_ sender: AnyObject) { print("status bar button clicked") if popover.isShown { hidePopover(sender) } else { showPopover(sender) } } func showPopover(_ sender: AnyObject) { // relativeTo 参数表示 popover 关联视图的边界 // of 参数表示 popover 要关联的视图 // preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现 popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY) } func hidePopover(_ sender: AnyObject) { popover.performClose(sender) } }
import AppKit

class StatusBarController {
    private var statusItem: NSStatusItem
    private var statusBarButton: NSStatusBarButton
    private var popover: NSPopover
    
    init(_ popover: NSPopover) {
        statusItem = NSStatusBar.system.statusItem(withLength: 28)
        statusBarButton = statusItem.button!
        self.popover = popover
        
        statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon")
        statusBarButton.image!.size = NSSize(width: 18, height: 18)
        
        // #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针”
        statusBarButton.action = #selector(togglePopover(_:))
        statusBarButton.target = self
    }
    
    // Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用
    @objc func togglePopover(_ sender: AnyObject)
    {
        print("status bar button clicked")
        
        if popover.isShown {
            hidePopover(sender)
        } else {
            showPopover(sender)
        }
    }
    
    func showPopover(_ sender: AnyObject)
    {
        // relativeTo 参数表示 popover 关联视图的边界
        // of 参数表示 popover 要关联的视图
        // preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现
        popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
    }
    
    func hidePopover(_ sender: AnyObject)
    {
        popover.performClose(sender)
    }
}

最后,需要修改 AppDelegate.swift,在创建 StatusBarController 与 NSPopover 实例。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
// 类型后面用 ! 号表示这是一个隐式解析的可选类型
var statusBar: StatusBarController!
var popover: NSPopover!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// 创建 ContentView 主视图实例,并附加持久化存储上下文环境
let context = persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
// 创建 NSPopover 类型实例
popover = NSPopover()
// 必须先为 NSPopover 设置视图控制器后才能添加视图
popover.contentViewController = ContentViewController()
popover.contentSize = NSSize(width: 360, height: 360)
// 这里用 ? 问号表示是一个可选链式调用。如果改用 ! 的话则表示强制解包,强制解包的链式调用遇到 nil 时会报错
popover.contentViewController?.view = NSHostingView(rootView: contentView)
// 创建状态栏图标控制器
statusBar = StatusBarController(popover)
}
// ...
@NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { // 类型后面用 ! 号表示这是一个隐式解析的可选类型 var statusBar: StatusBarController! var popover: NSPopover! func applicationDidFinishLaunching(_ aNotification: Notification) { // 创建 ContentView 主视图实例,并附加持久化存储上下文环境 let context = persistentContainer.viewContext let contentView = ContentView().environment(\.managedObjectContext, context) // 创建 NSPopover 类型实例 popover = NSPopover() // 必须先为 NSPopover 设置视图控制器后才能添加视图 popover.contentViewController = ContentViewController() popover.contentSize = NSSize(width: 360, height: 360) // 这里用 ? 问号表示是一个可选链式调用。如果改用 ! 的话则表示强制解包,强制解包的链式调用遇到 nil 时会报错 popover.contentViewController?.view = NSHostingView(rootView: contentView) // 创建状态栏图标控制器 statusBar = StatusBarController(popover) } // ...
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    // 类型后面用 ! 号表示这是一个隐式解析的可选类型
    var statusBar: StatusBarController!
    var popover: NSPopover!


    func applicationDidFinishLaunching(_ aNotification: Notification) {
        
        // 创建 ContentView 主视图实例,并附加持久化存储上下文环境
        let context = persistentContainer.viewContext
        let contentView = ContentView().environment(\.managedObjectContext, context)
        
        // 创建 NSPopover 类型实例
        popover = NSPopover()
        // 必须先为 NSPopover 设置视图控制器后才能添加视图
        popover.contentViewController = ContentViewController()
        popover.contentSize = NSSize(width: 360, height: 360)
        // 这里用 ? 问号表示是一个可选链式调用。如果改用 ! 的话则表示强制解包,强制解包的链式调用遇到 nil 时会报错
        popover.contentViewController?.view = NSHostingView(rootView: contentView)
        
        // 创建状态栏图标控制器
        statusBar = StatusBarController(popover)

    }

    // ...

编译运行程序,这时候点击状态栏图标就会弹出 ContentView 视图,再次点击则会关闭弹出视图。

5.2 监听外部事件 NSEvent

对于状态栏图标程序,我们还需要一个基本功能就是,当 popover 视图打开的时候,用户点击桌面的其他地方也能关闭 popover 视图。要实现这一功能,我们需要添加一个系统全局事件监听器,当监听到用户在程序外部点击鼠标时触发事件监听器的处理方法。

新建一个 EventMonitor 类:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import Cocoa
class EventMonitor {
private var monitor: Any?
private let mask: NSEvent.EventTypeMask
private let handler: (NSEvent?) -> Void
// mask 是要监听的事件类型
// handler 是事件处理函数,它是一个逃逸闭包,接受一个 NSEvent? 类型作为参数,返回 Void(无返回值)
public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
self.mask = mask
self.handler = handler
}
deinit {
stop()
}
public func start() {
// 添加一个系统全局事件监听器,并返回给 monitor 存储属性
// as! 表示将前面的可选类型当作 NSObject 进行强制解包
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) as! NSObject
}
public func stop() {
if monitor != nil {
// 从系统全局事件监听器队列中删除自己的监听器
NSEvent.removeMonitor(monitor!)
// 解除引用,使得该事件监听器实例被销毁
monitor = nil
}
}
}
import Cocoa class EventMonitor { private var monitor: Any? private let mask: NSEvent.EventTypeMask private let handler: (NSEvent?) -> Void // mask 是要监听的事件类型 // handler 是事件处理函数,它是一个逃逸闭包,接受一个 NSEvent? 类型作为参数,返回 Void(无返回值) public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { self.mask = mask self.handler = handler } deinit { stop() } public func start() { // 添加一个系统全局事件监听器,并返回给 monitor 存储属性 // as! 表示将前面的可选类型当作 NSObject 进行强制解包 monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) as! NSObject } public func stop() { if monitor != nil { // 从系统全局事件监听器队列中删除自己的监听器 NSEvent.removeMonitor(monitor!) // 解除引用,使得该事件监听器实例被销毁 monitor = nil } } }
import Cocoa

class EventMonitor {
    private var monitor: Any?
    private let mask: NSEvent.EventTypeMask
    private let handler: (NSEvent?) -> Void
    
    // mask 是要监听的事件类型
    // handler 是事件处理函数,它是一个逃逸闭包,接受一个 NSEvent? 类型作为参数,返回 Void(无返回值)
    public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
      self.mask = mask
      self.handler = handler
    }

    deinit {
      stop()
    }

    public func start() {
        // 添加一个系统全局事件监听器,并返回给 monitor 存储属性
        // as! 表示将前面的可选类型当作 NSObject 进行强制解包
        monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) as! NSObject
    }

    public func stop() {
      if monitor != nil {
        // 从系统全局事件监听器队列中删除自己的监听器
        NSEvent.removeMonitor(monitor!)
        // 解除引用,使得该事件监听器实例被销毁
        monitor = nil
      }
    }
}

然后需要修改 StatusBarController.swift,在 StatusBarController 中添加新的属性声明:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
private var eventMonitor: EventMonitor?
private var eventMonitor: EventMonitor?
private var eventMonitor: EventMonitor?

在 StatusBarController 构造函数的末尾添加对 eventMonitor 属性的赋值语句:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: outerClickedHandler)
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: outerClickedHandler)
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: outerClickedHandler)

在 StatusBarController 类中新增事件处理方法 outerClickedHandler 并修改原来的显示隐藏弹出框的方法:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
func showPopover(_ sender: AnyObject) {
// relativeTo 参数表示 popover 关联视图的边界
// of 参数表示 popover 要关联的视图
// preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现
popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
eventMonitor?.start()
}
func hidePopover(_ sender: AnyObject) {
popover.performClose(sender)
eventMonitor?.stop()
}
func outerClickedHandler(_ event: NSEvent?) {
if(popover.isShown)
{
hidePopover(event!)
}
}
func showPopover(_ sender: AnyObject) { // relativeTo 参数表示 popover 关联视图的边界 // of 参数表示 popover 要关联的视图 // preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现 popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY) eventMonitor?.start() } func hidePopover(_ sender: AnyObject) { popover.performClose(sender) eventMonitor?.stop() } func outerClickedHandler(_ event: NSEvent?) { if(popover.isShown) { hidePopover(event!) } }
func showPopover(_ sender: AnyObject) {
    // relativeTo 参数表示 popover 关联视图的边界
    // of 参数表示 popover 要关联的视图
    // preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现
    popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
    eventMonitor?.start()
}

func hidePopover(_ sender: AnyObject) {
    popover.performClose(sender)
    eventMonitor?.stop()
}

func outerClickedHandler(_ event: NSEvent?) {
    if(popover.isShown)
    {
        hidePopover(event!)
    }
}

编译运行现在的项目,OK,这就是一个状态栏程序的基础模版了。

5.3 监听外部事件 NSEvent (更新)

在 5.2 中,我们监听到程序外部的鼠标点击事件后,通过 NSPopover.performClose() 来关闭弹出窗口,随之就停止了事件监听器。但这里其实存在一个 Bug,因为 NSPopover.performClose() 并不保证关闭,官方文档中有说明:The operation will fail if the popover is displaying a nested popover or if it has a child window.

而我也确实遇到了这个情况:在弹出窗口中添加了一个按钮,并且给该按钮设置了 .popover() 修饰器,使得点击该按钮会弹出一个子弹出框/这时候如果再点击程序外部,也会触发 5.2 中定义的 outerClickedHandler() 函数,并执行 NSPopover.performClose() 与 eventMonitor?.stop()。但此时 performClose() 只是关闭了子弹出窗,主弹出窗本身并不关闭。而我们又执行了eventMonitor?.stop(),那么后面再怎么点击程序外部,都不会关闭主弹出窗口了(点击状态栏按钮还行,不影响)。

正确的做法,应该是把停止事件监听器的动作,放在监控到 NSPopover 真正关闭之后才执行。如果监控 NSPopover 是否关闭呢?其实有一个很好的做法:我在查看了 NSPopover 的文档之后,发现它会在显示、关闭的时候向 NotificationCenter 发出 NSPopover.didShowNotification、NSPopover.didCloseNotification 这些通知(NSWindow 也有这样的行为)。那么就能通过订阅这些通知来执行停止外部点击监听器的操作。

修改后的 StatusBarController.swift 代码如下:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import AppKit
import Combine
class StatusBarController {
private var statusItem: NSStatusItem
private var statusBarButton: NSStatusBarButton
private var popover: NSPopover
private var eventMonitor: EventMonitor?
private var subscribePopoverDidClose: AnyCancellable?
private var subscribePopoverDidShow: AnyCancellable?
init(_ popover: NSPopover) {
self.statusItem = NSStatusBar.system.statusItem(withLength: 30)
self.statusBarButton = statusItem.button!
self.popover = popover
// 设置状态栏图标
statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon")
statusBarButton.image!.size = NSSize(width: 18, height: 18)
// 设置状态栏图标点击事件
// #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针”
statusBarButton.action = #selector(togglePopover(_:))
statusBarButton.target = self
// 设置一个事件监听器,监听鼠标在程序外部的点击事件
self.eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: outerClickedHandler)
self.subscribePopoverDidShow = NotificationCenter.default.publisher(for: NSPopover.didShowNotification, object: popover).sink(receiveValue: { _ in
log.debug("收到弹出窗口已打开的消息")
self.eventMonitor?.start()
})
self.subscribePopoverDidClose = NotificationCenter.default.publisher(for: NSPopover.didCloseNotification, object: popover).sink(receiveValue: { _ in
log.debug("收到弹出窗口已关闭的消息")
self.eventMonitor?.stop()
})
}
// Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用
@objc func togglePopover(_ sender: AnyObject)
{
if popover.isShown {
hidePopover(sender)
} else {
showPopover(sender)
}
}
func showPopover(_ sender: AnyObject) {
log.debug("显示弹出窗口")
// relativeTo 参数表示 popover 关联视图的边界
// of 参数表示 popover 要关联的视图
// preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现
popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
func hidePopover(_ sender: AnyObject) {
log.debug("关闭弹出窗口")
popover.performClose(sender)
}
func outerClickedHandler(_ event: NSEvent?) {
log.debug("监听到程序外部的点击事件")
if(popover.isShown)
{
hidePopover(event!)
}
}
}
import AppKit import Combine class StatusBarController { private var statusItem: NSStatusItem private var statusBarButton: NSStatusBarButton private var popover: NSPopover private var eventMonitor: EventMonitor? private var subscribePopoverDidClose: AnyCancellable? private var subscribePopoverDidShow: AnyCancellable? init(_ popover: NSPopover) { self.statusItem = NSStatusBar.system.statusItem(withLength: 30) self.statusBarButton = statusItem.button! self.popover = popover // 设置状态栏图标 statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon") statusBarButton.image!.size = NSSize(width: 18, height: 18) // 设置状态栏图标点击事件 // #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针” statusBarButton.action = #selector(togglePopover(_:)) statusBarButton.target = self // 设置一个事件监听器,监听鼠标在程序外部的点击事件 self.eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: outerClickedHandler) self.subscribePopoverDidShow = NotificationCenter.default.publisher(for: NSPopover.didShowNotification, object: popover).sink(receiveValue: { _ in log.debug("收到弹出窗口已打开的消息") self.eventMonitor?.start() }) self.subscribePopoverDidClose = NotificationCenter.default.publisher(for: NSPopover.didCloseNotification, object: popover).sink(receiveValue: { _ in log.debug("收到弹出窗口已关闭的消息") self.eventMonitor?.stop() }) } // Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用 @objc func togglePopover(_ sender: AnyObject) { if popover.isShown { hidePopover(sender) } else { showPopover(sender) } } func showPopover(_ sender: AnyObject) { log.debug("显示弹出窗口") // relativeTo 参数表示 popover 关联视图的边界 // of 参数表示 popover 要关联的视图 // preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现 popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY) } func hidePopover(_ sender: AnyObject) { log.debug("关闭弹出窗口") popover.performClose(sender) } func outerClickedHandler(_ event: NSEvent?) { log.debug("监听到程序外部的点击事件") if(popover.isShown) { hidePopover(event!) } } }
import AppKit
import Combine

class StatusBarController {
    private var statusItem: NSStatusItem
    private var statusBarButton: NSStatusBarButton
    private var popover: NSPopover
    
    private var eventMonitor: EventMonitor?
    private var subscribePopoverDidClose: AnyCancellable?
    private var subscribePopoverDidShow: AnyCancellable?
    
    init(_ popover: NSPopover) {
        self.statusItem = NSStatusBar.system.statusItem(withLength: 30)
        self.statusBarButton = statusItem.button!
        self.popover = popover
        
        // 设置状态栏图标
        statusBarButton.image = NSImage(imageLiteralResourceName: "StatusBarIcon")
        statusBarButton.image!.size = NSSize(width: 18, height: 18)
        
        // 设置状态栏图标点击事件
        // #selector(func) 语法糖生成一个 Selector 实例,它对应 Object-C 的 SEL 类型,实际上就是“函数指针”
        statusBarButton.action = #selector(togglePopover(_:))
        statusBarButton.target = self
        
        // 设置一个事件监听器,监听鼠标在程序外部的点击事件
        self.eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: outerClickedHandler)
        
        self.subscribePopoverDidShow = NotificationCenter.default.publisher(for: NSPopover.didShowNotification, object: popover).sink(receiveValue: { _ in
            log.debug("收到弹出窗口已打开的消息")
            self.eventMonitor?.start()
        })
        
        self.subscribePopoverDidClose = NotificationCenter.default.publisher(for: NSPopover.didCloseNotification, object: popover).sink(receiveValue: { _ in
            log.debug("收到弹出窗口已关闭的消息")
            self.eventMonitor?.stop()
        })
    }
    
    // Swift 中的 @objc 特性表示表示这个声明可以被 Object-C 代码调用
    @objc func togglePopover(_ sender: AnyObject)
    {
        if popover.isShown {
            hidePopover(sender)
        } else {
            showPopover(sender)
        }
    }
    
    func showPopover(_ sender: AnyObject) {
        log.debug("显示弹出窗口")
        
        // relativeTo 参数表示 popover 关联视图的边界
        // of 参数表示 popover 要关联的视图
        // preferredEdge 参数表示 popover 的箭头要在关联视图的哪一边出现
        popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
    }

    func hidePopover(_ sender: AnyObject) {
        log.debug("关闭弹出窗口")
        popover.performClose(sender)
    }

    func outerClickedHandler(_ event: NSEvent?) {
        log.debug("监听到程序外部的点击事件")
        if(popover.isShown)
        {
            hidePopover(event!)
        }
    }
}

 

Leave a Comment

Your email address will not be published. Required fields are marked *