macOS的menuBar功能非常强大,我们在开发macOS应用的时候,经常需要利用menuBar实现功能。然而网上关于menuBar开发的文档却甚少,更别提用上SwiftUI开发的。这篇文章从以下几部分讲解用SwiftUI开发menuBar/statusBar的方法,希望对大家有帮助:
一、在statusBarItem弹出Popover
二、更改Popover背景色
三、在statusBar上控制主窗口的开关
四、在statusBar上控制statusBar的显隐/开关
statusBar弹出menu是比较简单的,但是如果我们要做一些定制,就需要用到Popover了
在AppDelegate文件中,声明一个NSPopover变量(下面的statusBarItem之后会用到)
var popover: NSPopover! var statusBarItem: NSStatusItem!随后,在applicationDidFinishLaunching中, ContentView 初始化之后,加上
// Create the popover let popover = NSPopover() popover.contentSize = NSSize(width: 400, height: 400) popover.behavior = .transient //代表用户点击其他区域时popover自动消失 popover.contentViewController = NSHostingController(rootView: contentView) self.popover = popover此时我们的AppDelegate文件应该长这样:
import Cocoa import SwiftUI @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { var popover: NSPopover! func applicationDidFinishLaunching(_ aNotification: Notification) { // Create the SwiftUI view that provides the window contents. let contentView = ContentView() // Create the popover let popover = NSPopover() popover.contentSize = NSSize(width: 400, height: 500) popover.behavior = .transient popover.contentViewController = NSHostingController(rootView: contentView) self.popover = popover } }
这样我们即创建了一个Popover视图以及它的控制器,这个Popover的视图内容是我们的ContentView.swift里的内容。这样我们就可以在ContentView里用SwiftUI自由地定制我们的Popover。接下来我们要做的是,创建一个menuBarItem,当用户点击menuBarItem的按钮时,弹出我们已经写好的Popover。
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength)) if let button = self.statusBarItem.button { button.image = NSImage(named: "Icon") button.action = #selector(togglePopover(_:)) }还记得我们一开始声明了
var statusBarItem: NSStatusItem!现在我们需要创建它,同时给它在menuBar设置个图标,绑定点击事件为togglePopover
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength)) if let button = self.statusBarItem.button { statusBarItem.button?.title = "⏳" button.action = #selector(togglePopover(_:)) }绑定togglePopover是为了在用户点击时弹出Popover
// Create the status item @objc func togglePopover(_ sender: AnyObject?) { if let button = self.statusBarItem.button { if self.popover.isShown { self.popover.performClose(sender) } else { self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) } } }最终,我们的AppDelegate文件长这样
import Cocoa import SwiftUI @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { var popover: NSPopover! var statusBarItem: NSStatusItem! func applicationDidFinishLaunching(_ aNotification: Notification) { // Create the SwiftUI view that provides the window contents. let contentView = ContentView() // Create the popover let popover = NSPopover() popover.contentSize = NSSize(width: 400, height: 500) popover.behavior = .transient popover.contentViewController = NSHostingController(rootView: contentView) self.popover = popover // Create the status item self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength)) if let button = self.statusBarItem.button { statusBarItem.button?.title = "⏳" button.action = #selector(togglePopover(_:)) } } @objc func togglePopover(_ sender: AnyObject?) { if let button = self.statusBarItem.button { if self.popover.isShown { self.popover.performClose(sender) } else { self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) } } } }
我们创建出来的Popover默认的背景色是透明的,很容易与我们的UI不搭配。如果需要定制背景颜色,目前我没有发现直接用SwiftUI实现的方法。我们需要去拓展NSPopover类,在新建实例的时候指定背景色。
首先,拓展NSPopover类:
import Cocoa import SwiftUI extension NSPopover { private struct Keys { static var backgroundViewKey = "backgroundKey" } private var backgroundView: NSView { let bgView = objc_getAssociatedObject(self, &Keys.backgroundViewKey) as? NSView if let view = bgView { return view } let view = NSView() objc_setAssociatedObject(self, &Keys.backgroundViewKey, view, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) NotificationCenter.default.addObserver(self, selector: #selector(popoverWillOpen(_:)), name: NSPopover.willShowNotification, object: nil) return view } @objc private func popoverWillOpen(_ notification: Notification) { if backgroundView.superview == nil { if let contentView = contentViewController?.view, let frameView = contentView.superview { frameView.wantsLayer = true backgroundView.frame = NSInsetRect(frameView.frame, 1, 1) backgroundView.autoresizingMask = [.width, .height] frameView.addSubview(backgroundView, positioned: .below, relativeTo: contentView) } } } var backgroundColor: NSColor? { get { if let bgColor = backgroundView.layer?.backgroundColor { return NSColor(cgColor: bgColor) } return nil } set { backgroundView.wantsLayer = true backgroundView.layer?.backgroundColor = newValue?.cgColor } } }在AppDelegate初始化时,增加background参数
let popover = NSPopover() popover.backgroundColor = NSColor.white //设置popover颜色,对应extension里对popover的改写 popover.contentSize = NSSize(width: 350, height: 400)搞定!
现在statusBarItem的视图文件中加入按钮
HStack{ Text("Time Capsule").font(Font.system(.headline)).bold() Spacer() MenuButton(label: Image("moreBlack").resizable().frame(width:20,height:20)) { Button(action: { showWindow() }) { Text("打开主窗口") } Button(action: { hideStatusBar() }) { Text("退出") } }.menuButtonStyle(BorderlessButtonMenuButtonStyle()) } .padding([.top, .trailing], 18).padding(.leading,30)接下来我们要补充两个按钮点击事件showWindow()和hideStatusBar()的方法
func showWindow() { NSApp.unhide(nil) func hideStatusBar() { let appDelegate = NSApplication.shared.delegate as! AppDelegate appDelegate.statusBarItem.isVisible = false }NSApp.unhide方法可以直接显示我们的主程序,hide则可以隐藏主程序
statusBarItem.isVisible设为false则可以直接让我们的插件消失