可以控制独自的展开与收起,也可以控制全部的展开与收起,全部代码:
//
// TreeListView.swift
// TreeListView
//
// Created by xuewu wei on 2022/9/4.
//
import SwiftUI
struct TreeListView: View {
@StateObject var vm: NodeManager = NodeManager()
@State var isExpanded: Bool = false
var body: some View {
VStack {
List {
ForEach($vm.list, id: \.self) { $item in
NodeGroup(node: item, childKeyPath: \.children, expandList: $vm.expandList) { item in
if ((item.children?.isEmpty) != nil) {
Group {
Image(systemName: "folder.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.blue)
.frame(width: 30, height: 30)
Text(item.name)
}
}else {
HStack {
Image(systemName: "doc.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.blue)
.frame(width: 30, height: 30)
Text(item.name)
}
}
}
}
}
Button("全部展开/收起") {
self.isExpanded = !self.isExpanded
self.vm.expandAllOrCloseAll(self.isExpanded)
print("=====: ",self.isExpanded, self.vm.expandList)
}
}
}
}
struct TreeListView_Previews: PreviewProvider {
static var previews: some View {
TreeListView()
}
}
struct NodeGroup<Node, Content: View>: View where Node: Hashable, Node: Identifiable, Node: CustomStringConvertible{
@State var node: Node
let childKeyPath: KeyPath<Node, [Node]?>
@Binding var expandList: [String:Bool]
@ViewBuilder let content: (_ item: Node) -> Content
var body: some View {
if node[keyPath: childKeyPath] != nil {
let expanded = Binding<Bool>(get: {expandList[node.id as! String] ?? false}, set: {expandList[node.id as! String] = $0})
DisclosureGroup(
isExpanded: expanded,
content: {
if expanded.wrappedValue {
ForEach(node[keyPath: childKeyPath]!) { childNode in
NodeGroup(node: childNode, childKeyPath: childKeyPath, expandList: $expandList) { item in
content(item)
}
}
}
}) {
content(node)
}
} else {
content(node)
}
}
}
// Mark: 数据
struct Node: Identifiable, Hashable, CustomStringConvertible {
var id: String = UUID().uuidString
var name: String
var isExpanded: Bool = false
var children: [Node]?
var description: String {
switch children {
case nil:
return "? \(name)"
case .some(let children):
return children.isEmpty ? "? \(name)" : "? \(name)"
}
}
}
class NodeManager: ObservableObject {
@Published var list: [Node] = []
@Published var expandList: [String:Bool] = [:] // 控制展开的字典值集合
init() {
self.list = [
Node(name: "哈哈--1", children: [
Node(name: "哈哈子集--1"),
Node(name: "哈哈子集--2", children: [
Node(name: "哈哈子集--2--1"),
Node(name: "哈哈子集--2--2")
]),
Node(name: "哈哈子集--3", children: [
Node(name: "哈哈子集--3--1"),
Node(name: "哈哈子集--3--2")
])
]),
Node(name: "呵呵--1", children: [
Node(name: "呵呵子集--1"),
Node(name: "呵呵子集--2", children: [
Node(name: "呵呵子集--2--1"),
Node(name: "呵呵子集--2--2")
]),
])
]
self.expandList = getExpandArr(arr: self.list)
}
// 根据数据抽出一个控制列表展开与收起的字典
func getExpandArr(arr: [Node?]) -> [String:Bool] {
var totals: [String:Bool] = [:]
for item in arr {
if let obj = item {
// 保留旧键
totals.merge([obj.id: false]) { current, _ in
current
}
if ((obj.children?.isEmpty) != nil) {
totals.merge(getExpandArr(arr: obj.children!)) { current, _ in
current
}
}
}
}
return totals
}
// 全部展开或者收起
func expandAllOrCloseAll(_ flag: Bool) {
for key in self.expandList.keys {
self.expandList.updateValue(flag, forKey: key)
}
}
}
重点有三:
- 一个控制展开与收起的
[String:Bool]
字典expandList
集合 - 此行代码
let expanded = Binding<Bool>(get: {expandList[node.id as! String] ?? false}, set: {expandList[node.id as! String] = $0})
DisclosureGroup
组件的isExpanded
属性是Binding<Bool>
类>型,所以需要把控制展开与收起集合里字典对象包装一下,以符合组件需要。
- 显而易见就是树形的递归调用了