如何在SwiftUI生命周期中使用NSToolbar实现可编辑多列布局适配
实现方案
你遇到的工具栏修改后重置的问题,核心原因是SwiftUI原生Toolbar在视图重绘时会重新加载默认配置,没有持久化用户自定义的修改。你可以通过桥接AppKit的NSToolbar,配合持久化配置存储实现符合需求的可编辑工具栏,同时天然适配多列布局:
1. 定义工具栏配置持久化模型
用UserDefaults存储用户自定义的工具栏项顺序、可见性,避免视图刷新时配置被重置:
struct ToolbarItemConfig: Codable, Identifiable { let id: String var isVisible: Bool var order: Int } class ToolbarPreferences: ObservableObject { static let shared = ToolbarPreferences() private let defaultsKey = "CustomToolbarConfig" // 预设默认工具栏项,可按需求扩展 @Published var items: [ToolbarItemConfig] = [ ToolbarItemConfig(id: "sidebarToggle", isVisible: true, order: 0), ToolbarItemConfig(id: "newItem", isVisible: true, order: 1), ToolbarItemConfig(id: "share", isVisible: true, order: 2), ToolbarItemConfig(id: "search", isVisible: true, order: 3) ] init() { loadConfig() } func saveConfig() { guard let data = try? JSONEncoder().encode(items) else { return } UserDefaults.standard.set(data, forKey: defaultsKey) } func loadConfig() { guard let data = UserDefaults.standard.data(forKey: defaultsKey), let savedItems = try? JSONDecoder().decode([ToolbarItemConfig].self, from: data) else { return } items = savedItems } }
2. 封装NSToolbar桥接层
用NSViewControllerRepresentable桥接NSToolbar,遵循NSToolbarDelegate协议实现可编辑能力:
struct CustomNSToolbar: NSViewControllerRepresentable { @ObservedObject var preferences = ToolbarPreferences.shared var window: NSWindow? func makeNSViewController(context: Context) -> NSViewController { let controller = NSViewController() let toolbar = NSToolbar(identifier: "CustomMainToolbar") toolbar.delegate = context.coordinator toolbar.allowsUserCustomization = true toolbar.autosavesConfiguration = true toolbar.displayMode = .iconAndLabel window?.toolbar = toolbar window?.toolbar?.validateVisibleItems() return controller } func updateNSViewController(_ nsViewController: NSViewController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, NSToolbarDelegate { var parent: CustomNSToolbar init(_ parent: CustomNSToolbar) { self.parent = parent } func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, canBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { let item = NSToolbarItem(itemIdentifier: itemIdentifier) switch itemIdentifier.rawValue { case "sidebarToggle": item.view = NSHostingView(rootView: Button { NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) } label: { Image(systemName: "sidebar.left") }) item.label = "侧边栏" case "newItem": item.view = NSHostingView(rootView: Button { // 补充新建项业务逻辑 } label: { Image(systemName: "plus") }) item.label = "新建" // 按需求补充其余工具栏项的实现 default: return nil } return item } func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { parent.preferences.items .sorted(by: { $0.order < $1.order }) .filter(\.isVisible) .map { NSToolbarItem.Identifier(rawValue: $0.id) } } func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { parent.preferences.items.map { NSToolbarItem.Identifier(rawValue: $0.id) } } func toolbarDidRemoveItem(_ notification: Notification) { saveConfig(notification: notification) } func toolbarDidAddItem(_ notification: Notification) { saveConfig(notification: notification) } func toolbarDidMoveItem(_ notification: Notification) { saveConfig(notification: notification) } private func saveConfig(notification: Notification) { guard let toolbar = notification.object as? NSToolbar else { return } let visibleIds = toolbar.items.map { $0.itemIdentifier.rawValue } for i in parent.preferences.items.indices { parent.preferences.items[i].isVisible = visibleIds.contains(parent.preferences.items[i].id) parent.preferences.items[i].order = visibleIds.firstIndex(of: parent.preferences.items[i].id) ?? 999 } parent.preferences.saveConfig() } } }
3. 适配多列布局
配合SwiftUI原生NavigationSplitView使用即可,2列/3列布局都可以直接适配:
@main struct MyApp: App { @State private var window: NSWindow? var body: some Scene { WindowGroup { // 3列布局,去掉content参数即可切换为2列布局 NavigationSplitView { SidebarView() // 第一列侧边栏 } content: { ContentColumnView() // 第二列内容列表 } detail: { DetailView() // 第三列详情页 } .background(WindowAccessor(window: $window)) .overlay { if let window = window { CustomNSToolbar(window: window) .frame(width: 0, height: 0) } } } } } // 辅助获取当前窗口实例的工具 struct WindowAccessor: NSViewRepresentable { @Binding var window: NSWindow? func makeNSView(context: Context) -> NSView { let view = NSView() DispatchQueue.main.async { window = view.window } return view } func updateNSView(_ nsView: NSView, context: Context) {} }
效果演示

内容的提问来源于stack exchange,提问作者Roman Banks
相关产品推荐
相关产品推荐

