You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 07:45:00