SwiftUI 如何编程实现层级List的全部展开/折叠功能
SwiftUI 树形List全局展开/折叠实现方案
不需要自定义递归视图,原生List+OutlineGroup原生能力即可实现,核心是接管系统的展开状态管理。
核心实现思路
- 系统简写的
List(_:children:content:)树形写法会自动管理节点展开状态,没有暴露全局控制的绑定接口,因此需要显式使用支持展开状态绑定的OutlineGroup组件嵌入List - 用
Set<UUID>类型的状态存储所有当前处于展开状态的节点ID:节点ID在集合内则为展开状态,不在则为折叠状态 - 写递归方法遍历整棵树,拿到所有带子节点的可展开节点ID集合:点击「全部展开」时把所有可展开ID写入状态集合,点击「全部折叠」时直接清空状态集合即可
完整可运行代码
import SwiftUI struct ContentView: View { // 树形数据源 @State private var tree: [MenuItem] = sampleMenuItems // 存储所有展开状态的节点ID @State private var expandedNodeIDs: Set<UUID> = [] init() { // 若需要默认全展开,解开下面注释即可 // _expandedNodeIDs = State(initialValue: getAllExpandableIDs(from: sampleMenuItems)) } var body: some View { NavigationView { List { // 显式使用OutlineGroup,绑定展开状态集合 OutlineGroup(tree, id: \.id, children: \.subMenuItems, isExpanded: $expandedNodeIDs) { node in Text(node.name) } } .navigationTitle("设备菜单") .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button("全部展开") { expandedNodeIDs = getAllExpandableIDs(from: tree) } Button("全部折叠") { expandedNodeIDs.removeAll() } } } } } // 递归获取所有可展开(带子节点)的节点ID private func getAllExpandableIDs(from nodes: [MenuItem]) -> Set<UUID> { var ids = Set<UUID>() for node in nodes { guard let subNodes = node.subMenuItems, !subNodes.isEmpty else { continue } ids.insert(node.id) // 递归加入子节点的可展开ID ids.formUnion(getAllExpandableIDs(from: subNodes)) } return ids } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } // 原有模型保持不变 struct MenuItem: Identifiable { var id = UUID() var name: String var subMenuItems: [MenuItem]? } // 示例数据 var espressoMachines = MenuItem(name: "Espresso Machines", subMenuItems: espressoMachineMenuItems) var grinders = MenuItem(name: "Grinders", subMenuItems: grinderMenuItems) var otherEquipment = MenuItem(name: "Other Equipment", subMenuItems: otherMenuItems) let sampleMenuItems = [ espressoMachines, grinders, otherEquipment] // Espresso Machines 子菜单 let espressoMachineMenuItems = [ MenuItem(name: "Leva", subMenuItems: [ MenuItem(name: "Leva X"), MenuItem(name: "Leva S") ]), MenuItem(name: "Strada", subMenuItems: [ MenuItem(name: "Strada EP" ), MenuItem(name: "Strada AV"), MenuItem(name: "Strada MP"), MenuItem(name: "Strada EE") ]), MenuItem(name: "KB90"), MenuItem(name: "Linea", subMenuItems: [ MenuItem(name: "Linea PB X"), MenuItem(name: "Linea PB"), MenuItem(name: "Linea Classic") ]), MenuItem(name: "GB5"), MenuItem(name: "Home", subMenuItems: [ MenuItem(name: "GS3"), MenuItem(name: "Linea Mini") ]) ] // Grinders 子菜单 let grinderMenuItems = [ MenuItem(name: "Swift"), MenuItem(name: "Vulcano"), MenuItem(name: "Swift Mini"), MenuItem(name: "Lux D") ] // 其他设备子菜单 let otherMenuItems = [ MenuItem(name: "Espresso AV"), MenuItem(name: "Espresso EP"), MenuItem(name: "Pour Over"), MenuItem(name: "Steam") ]
补充说明
- 不需要自己实现
DisclosureGroup的递归逻辑,OutlineGroup已经封装了完整的层级渲染和折叠交互,只接管状态绑定即可,性能和原生简写写法完全一致 - 如果需要记忆用户手动展开/折叠的状态,只需要把
expandedNodeIDs做持久化存储即可,不影响全局展开/折叠功能 - 低版本系统兼容只需要对应调整导航栏相关API,核心展开控制逻辑不受影响
内容的提问来源于stack exchange,提问作者David
相关产品推荐
相关产品推荐

