如何在 SwiftUI macOS 侧栏切换按钮中集成下拉菜单?
结论
可以用带 primaryAction 的 Menu 实现:点击主体切换侧栏,点击菜单指示器打开菜单。将它放入 ToolbarItem,再移除默认的 .sidebarToggle,就能避免出现两个侧栏按钮。
替换按钮后,SwiftUI 会重新决定工具栏布局。这个方案使用系统菜单控件,但尺寸、间距和位置未必与默认侧栏按钮完全一致。如果必须使用 macOS 原生的分段菜单工具栏项,可以考虑 AppKit 的 NSMenuToolbarItem。
为什么 HStack 无法自动获得一体式样式?
HStack(spacing: 0) 只消除两个视图之间的布局间距。内部的 Button 和 Menu 仍是两个控件,各有自己的样式、内边距和点击区域。ControlGroup 也不会把它们变成默认侧栏按钮。
带 primaryAction 的 Menu 则可以让主体执行操作、指示器展开菜单。Apple 文档说明,点击主体会执行主操作,菜单通过次级交互打开,例如点击菜单指示器。Menu 文档
SwiftUI 实现:用一个 Menu 替换默认按钮
以下示例以 macOS 14 及以上为部署目标,使用双列 NavigationSplitView:
import SwiftUI
@available(macOS 14.0, *)
struct SidebarMenuExample: View {
@State private var columnVisibility: NavigationSplitViewVisibility = .all
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
List {
Text("Sidebar")
}
.toolbar(removing: .sidebarToggle)
} detail: {
Text("Detail")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar {
ToolbarItem(placement: .navigation) {
Menu {
Button("Show Sidebar") {
columnVisibility = .all
}
Button("Hide Sidebar") {
columnVisibility = .detailOnly
}
} label: {
Label("Toggle Sidebar", systemImage: "sidebar.left")
} primaryAction: {
toggleSidebar()
}
.labelStyle(.iconOnly)
.menuIndicator(.visible)
.help("Toggle sidebar or open sidebar options")
}
}
}
}
private func toggleSidebar() {
withAnimation {
columnVisibility = columnVisibility == .detailOnly
? .all
: .detailOnly
}
}
}
代码中有几处需要注意:
.toolbar(removing: .sidebarToggle)放在侧栏内容上,移除系统自动添加的按钮,与 Apple 文档中的用法一致。toolbar(removing:) 文档- 自定义工具栏项放在 detail 一侧,侧栏隐藏后仍可访问。
label中只放侧栏图标,由系统绘制菜单箭头,不再手动添加Image(systemName: "chevron.down")。
菜单中的显示和隐藏操作只是示例,可以替换成自己的菜单内容。columnVisibility 控制列的可见性,上述切换逻辑适用于双列布局。三列布局需要根据是否保留中间列另行处理。NavigationSplitView 文档
为什么位置仍可能与默认按钮不同?
.navigation 表示这是一个导航操作。在 macOS 上,系统将这类项目放在工具栏前端,但没有承诺使用默认侧栏按钮的内部槽位,也没有提供像素级坐标控制。ToolbarItemPlacement.navigation 文档
这是一个替代控件,仍需在目标应用中检查窗口工具栏样式、其他工具栏内容和 macOS 版本带来的影响。仅凭原问题提供的代码,无法确认某个具体外观已经完全复现。
需要原生分段工具栏项时:NSMenuToolbarItem
如果要求主体和箭头呈现为 macOS 工具栏中的原生分段控件,可以使用 NSMenuToolbarItem。设置 action 并将 showsIndicator 设为 true 后,系统会将菜单指示器显示为单独的可点击分段。NSMenuToolbarItem 文档
以下核心代码用于创建工具栏项,可集成到已有的 AppKit 工具栏中:
import AppKit
@MainActor
func makeSidebarMenuItem(
target: AnyObject,
action: Selector,
menu: NSMenu
) -> NSMenuToolbarItem {
let item = NSMenuToolbarItem(
itemIdentifier: NSToolbarItem.Identifier("SidebarMenu")
)
item.label = "Sidebar"
item.paletteLabel = "Sidebar"
item.toolTip = "Toggle sidebar or open sidebar options"
item.image = NSImage(
systemSymbolName: "sidebar.left",
accessibilityDescription: "Toggle Sidebar"
)
item.target = target
item.action = action
item.menu = menu
item.showsIndicator = true
return item
}
调用方需要提供可通过 Objective-C selector 调用的侧栏切换方法,以及包含实际菜单操作的 NSMenu,再通过自己管理的 NSToolbarDelegate 返回这个工具栏项。持有该操作方法的对象也需要由应用保留。
这段代码只负责创建工具栏项。NSMenuToolbarItem 本身是工具栏项,不是 NSView,不能直接作为 SwiftUI ToolbarItem 的内容使用。采用这个方案时,还需要管理窗口的 NSToolbar、项目顺序,以及与 SwiftUI columnVisibility 的状态同步。
如何选择?
如果重点是在一个工具栏项内完成两种点击操作,可以先用 Menu 的 primaryAction,减少手动拼接控件带来的样式问题。
如果原生分段外观和工具栏排列控制是硬性要求,可以采用 AppKit 工具栏集成。即使使用 NSMenuToolbarItem,也仍需配置项目位置,才能接近默认侧栏按钮的布局。
备注:内容仅供参考。
