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

SwiftUI Mac应用中如何验证并修改树形列表的选中项

实现SwiftUI树形列表的层级选中控制方案

针对你用SwiftUI List+嵌套DisclosureGroup实现Mac树形列表的需求,要实现「同层级可多选、父/子/孙项不可同时选中」的规则,可以通过以下步骤实现,替代NSOutlineView的代理拦截逻辑:

1. 统一节点模型的层级信息

因为仅靠UUID无法判断节点间的层级关系,给所有层级的模型统一补充两个关键属性(可以通过协议约束,不影响各模型的具体类型):

  • id: UUID:每个节点的唯一标识
  • allAncestorIDs: [UUID]:当前节点所有祖先的UUID数组(顶层节点为空数组,孙节点包含父、祖父的UUID)
  • childItems: [any TreeItem]:当前节点的子节点集合(用any适配不同模型类型)

示例协议及模型实现:

protocol TreeItem: Identifiable {
    var id: UUID { get }
    var allAncestorIDs: [UUID] { get }
    var childItems: [any TreeItem] { get }
    var title: String { get } // 显示用标题
}

// 顶层父节点示例
class ParentItem: TreeItem {
    let id = UUID()
    let title: String
    let childItems: [any TreeItem]
    
    var allAncestorIDs: [UUID] { [] }
    
    init(title: String, children: [any TreeItem]) {
        self.title = title
        self.childItems = children
    }
}

// 子节点示例
class ChildItem: TreeItem {
    let id = UUID()
    let title: String
    let parentID: UUID
    let childItems: [any TreeItem]
    
    var allAncestorIDs: [UUID] { [parentID] }
    
    init(title: String, parentID: UUID, children: [any TreeItem] = []) {
        self.title = title
        self.parentID = parentID
        self.childItems = children
    }
}

2. 用ObservableObject管理选中与展开状态

替代@State的选中集合,将选中逻辑封装到ViewModel中,通过方法拦截选中操作并校验规则:

class TreeSelectionManager: ObservableObject {
    @Published var selectedIDs: Set<UUID> = []
    @Published var expandedIDs: Set<UUID> = [] // 统一管理展开状态
    
    // 切换选中状态时自动校验规则
    func toggleSelection(for item: any TreeItem) {
        if selectedIDs.contains(item.id) {
            selectedIDs.remove(item.id)
            return
        }
        
        var newSelected = selectedIDs
        // 移除所有冲突的祖先节点
        newSelected.subtract(item.allAncestorIDs)
        // 移除所有冲突的后代节点
        newSelected.subtract(findAllDescendantIDs(for: item))
        // 添加当前节点
        newSelected.insert(item.id)
        
        selectedIDs = newSelected
    }
    
    // 递归查找所有后代节点的UUID
    private func findAllDescendantIDs(for item: any TreeItem) -> Set<UUID> {
        var ids: Set<UUID> = []
        for child in item.childItems {
            ids.insert(child.id)
            ids.formUnion(findAllDescendantIDs(for: child))
        }
        return ids
    }
    
    func isSelected(_ item: any TreeItem) -> Bool {
        selectedIDs.contains(item.id)
    }
}

3. 构建嵌套DisclosureGroup视图

通过自定义Binding将条目选中、展开状态与ViewModel绑定,确保所有操作经过规则校验:

struct TreeView: View {
    let rootItems: [any TreeItem]
    @StateObject private var selectionManager = TreeSelectionManager()
    
    var body: some View {
        List {
            ForEach(rootItems) { rootItem in
                TreeItemView(item: rootItem, selectionManager: selectionManager)
            }
        }
        .listStyle(.outline) // 匹配Mac平台OutlineView视觉风格
    }
}

struct TreeItemView: View {
    let item: any TreeItem
    @ObservedObject var selectionManager: TreeSelectionManager
    
    var body: some View {
        if !item.childItems.isEmpty {
            DisclosureGroup(isExpanded: expandedBinding) {
                ForEach(item.childItems) { childItem in
                    TreeItemView(item: childItem, selectionManager: selectionManager)
                }
            } label: {
                itemLabel
            }
        } else {
            itemLabel
        }
    }
    
    private var itemLabel: some View {
        HStack {
            Toggle(isOn: selectionBinding) {
                Text(item.title)
            }
            .toggleStyle(.checkbox) // Mac平台复选框样式
        }
    }
    
    // 自定义选中状态Binding,触发ViewModel的校验逻辑
    private var selectionBinding: Binding<Bool> {
        Binding(
            get: { selectionManager.isSelected(item) },
            set: { _ in selectionManager.toggleSelection(for: item) }
        )
    }
    
    // 自定义展开状态Binding
    private var expandedBinding: Binding<Bool> {
        Binding(
            get: { selectionManager.expandedIDs.contains(item.id) },
            set: { isExpanded in
                isExpanded ? selectionManager.expandedIDs.insert(item.id) : selectionManager.expandedIDs.remove(item.id)
            }
        )
    }
}

关键说明

  • 协议仅用于统一节点的关键属性,不替代各层级的具体模型类型,符合你的需求;
  • 所有选中操作都经过TreeSelectionManager的校验,确保父/子/孙项不会同时被选中;
  • 展开状态统一由ViewModel管理,解决了OutlineGroup无法自定义控制展开的问题;
  • 采用Mac平台原生的.outline列表样式和.checkbox开关样式,贴合系统体验。

内容的提问来源于stack exchange,提问作者Bryan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 06:04:53