如何使用SwiftUI在macOS中创建状态栏图标和菜单

32

什么是SwiftUI API用于创建状态栏菜单?

根据辅助检查器,苹果似乎在电池和WiFi菜单中使用SwiftUI视图。 附有电池菜单的屏幕截图,以及其视图层次结构。

Battery menu screenshot View hierarchy of the battery menu

编辑:

已将解决方案发布为单独的答案。


请问您能澄清一下这段代码具体放在AppDelegate的哪里吗?我遇到了崩溃以及状态栏图标短暂出现但很快就消失的问题。 - Dominic Holmes
它在哪里崩溃了?是在“NSStatusBar.system”这里吗? - Ruzard
没错,我解决了。我误解了你的回答,以为NSStatusBar是系统已经创建和提供的东西。 - Dominic Holmes
3个回答

27

由于这个问题最近受到了更多的关注,而且唯一的回答没有完全解决问题,我想重复我的问题的编辑部分,并将其标记为已解决。

编辑2:添加了一个额外的代码片段,允许使用SwiftUI视图作为状态栏图标。对于显示动态徽章可能很方便。

找到了一种在中显示它的方法,而不需要烦人的NSPopover。即使我使用了AppDelegate的applicationDidFinishLaunching来执行代码,它也可以从任何地方调用您的应用程序,甚至在SwiftUI生命周期应用程序中。

以下是代码:

func applicationDidFinishLaunching(_ aNotification: Notification) {
        // SwiftUI content view & a hosting view
        // Don't forget to set the frame, otherwise it won't be shown.
        //
        let contentViewSwiftUI = VStack {
            Color.blue
            Text("Test Text")
            Color.white
        }
        let contentView = NSHostingView(rootView: contentViewSwiftUI)
        contentView.frame = NSRect(x: 0, y: 0, width: 200, height: 200)
        
        // Status bar icon SwiftUI view & a hosting view.
        //
        let iconSwiftUI = ZStack(alignment:.center) {
            Rectangle()
                .fill(Color.green)
                .cornerRadius(5)
                .padding(2)
                
            Text("3")
                .background(
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 15, height: 15)
                )
                .frame(maxWidth: .infinity, maxHeight: .infinity,  alignment: .bottomTrailing)
                .padding(.trailing, 5)
        }
        let iconView = NSHostingView(rootView: iconSwiftUI)
        iconView.frame = NSRect(x: 0, y: 0, width: 40, height: 22)
        
        // Creating a menu item & the menu to add them later into the status bar
        //
        let menuItem = NSMenuItem()
        menuItem.view = contentView
        let menu = NSMenu()
        menu.addItem(menuItem)
        
        // Adding content view to the status bar
        //
        let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        statusItem.menu = menu
        
        // Adding the status bar icon
        //
        statusItem.button?.addSubview(iconView)
        statusItem.button?.frame = iconView.frame

        // StatusItem is stored as a property.
        self.statusItem = statusItem
    }

enter image description here


1
谢谢您!这正是我在寻找的。但是,我无法弄清楚如何使视图中的任何交互元素(按钮、文本字段)起作用。有什么想法吗? - Brad G.
1
@BradG。如果您在内容视图中添加一个按钮,它就可以交互了。在我的情况下,它会打印数据 https://gist.github.com/kmalyshev/7ef834efaeed83014f6ba851582fca0d 。您的情况中是否有任何阻止交互的因素? - Ruzard
1
我创建了一个最小化的项目来演示。TextField 最初确实可以工作,但在某个时候停止接受新输入(在其中键入任何内容都会关闭菜单)。菜单从未起作用。https://github.com/bgreenlee/MinimalMenuBarApp - Brad G.
1
很遗憾,我没有走得那么远。对我来说,有一些自定义视图就足够了。关于这个主题有一个文档。它谈到了这种方法的局限性。也许有一种方法可以克服这些局限性,手动处理鼠标和键盘事件。https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MenuList/Articles/ViewsInMenuItems.html#//apple_ref/doc/uid/TP40005166-SW1 - Ruzard
1
@eemrah 这些徽章通常需要手动渲染,stackoverflow上有绘制使用UIKit的示例。但是,在HostingView中使用相同的方法也可以用于图标。我更新了代码,请查看。 - Ruzard
显示剩余3条评论

11

MenuBarExtra(macOS Ventura)

在 macOS 13.0+ 和 Xcode 14.0+ 中,MenuBarExtra 结构体允许您创建一个类似于 NSStatusBar 图标和菜单的系统菜单栏。当您想要提供对常用功能的访问,即使您的应用程序不活动时,可以使用 MenuBarExtra

enter image description here

import SwiftUI

@available(macOS 13.0, *)                       // macOS Ventura
@main struct StatusBarApp: App {
    
    @State private var command: String = "a"
       
    var body: some Scene {

        MenuBarExtra(command, systemImage: "\(command).circle") {
           
            Button("Uno") { command = "a" }
                .keyboardShortcut("U")
           
            Button("Dos") { command = "b" }
                .keyboardShortcut("D")
           
            Divider()

            Button("Salir") { NSApplication.shared.terminate(nil) }
                .keyboardShortcut("S")
        }
    }
}

如何在 Dock 中隐藏应用程序图标

在 Xcode 的 Info 选项卡中,选择 Application is agent (UIElement) 并将其值设置为 YES

enter image description here

或者您可以通过编程的方式隐藏应用程序的图标programmatically


2
感谢更新。苹果花了一些时间来获取状态栏API。 我想补充你的回复 - 可以将任何SwiftUI视图添加到'MenuBarExtra',而不仅仅是按钮。 - Ruzard
1
你有没有想法,我们如何为 MenuBarExtra 设置最大帧?我已经尝试使用 defaultSize 修饰符,但似乎它不起作用。 - jackson89
2
当我使用自定义SVG图标时,它遇到了缩放问题 - 它看起来又丑又有锯齿 :( - Pavel Lobodinský
1
@RahulBir 我最终使用了经典的NSStatusItem。 - Pavel Lobodinský
1
@RahulBir 不确定我能否帮忙。我只是看了一些关于如何创建菜单栏应用程序的在线教程。如果在菜单栏项单击时打开窗口/弹出窗口,请尝试调用 NSApp.activate(ignoringOtherApps: true)appWindow?.makeKeyAndOrderFront(nil) - Pavel Lobodinský
显示剩余7条评论

9
在AppDelegate中添加以下代码:
// Create the status item in the Menu bar 
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))

// Add a menu and a menu item
let menu = NSMenu()
let editMenuItem = NSMenuItem()
editMenuItem.title = "Edit"
menu.addItem(editMenuItem)

//Set the menu 
self.statusBarItem.menu = menu

//This is the button which appears in the Status bar
if let button = self.statusBarItem.button {
    button.title = "Here"
}

这将在你的菜单栏中添加一个带有自定义菜单的按钮。

enter image description here

编辑 - 如何使用 SwiftUI 视图

如您所询问,以下是如何使用 SwiftUI 视图的答案。

首先创建一个 NSPopover,然后将您的 SwiftUI 视图包装在 NSHostingController 中。

var popover: NSPopover


let popover = NSPopover()
popover.contentSize = NSSize(width: 350, height: 350)
popover.behavior = .transient
popover.contentViewController = NSHostingController(rootView: contentView)
    self.popover = popover

然后,不再显示NSMenu,而是切换到弹出窗口:

self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
     button.title = "Click"
     button.action = #selector(showPopover(_:))
}

以下是需要执行的操作:

@objc func showPopover(_ 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)
        }
    }
}

enter image description here


6
很遗憾,您发布的代码是使用 AppKit 创建这些菜单的方式。它不是 SwiftUI 的方式,也不支持 SwiftIUI 视图。 - Ruzard
如果你使用AppKit App Delegate Life cycle创建一个项目,你仍然会得到一个AppDelegate。 - davidev
3
好的,但这不是我问的。我对在顶部菜单中展示SwiftUI 感兴趣。 - Ruzard
谢谢,我的问题最初的版本因为不够直接而被阻止,其中包含了关于NSPopover的一行。那个NSPopover似乎是一个我想要避免的解决方法。我非常感谢您的回复,一旦我能够给出赏金,我会尝试给您一个。您展示的代码肯定会帮助其他人。我会更新我的问题并提供更多细节以防万一。 - Ruzard
1
很高兴它有所帮助。我已经投票再次开放它,因为那是一个非常好的和有用的问题,我也在寻找答案。 - davidev
3
我添加了一个没有使用NSPopover的替代方案。也许您也会发现它很有用。 - Ruzard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接