Как сделать SwiftUI List/OutlineGroup ленивым для использования с большими деревьями, такими как файловая система?
Вот простая демонстрация иерархического
List
в SwiftUI. Я тестирую его на macOS Big Sur, но, в отличие от аналогичных древовидных компонентов в других инструментах пользовательского интерфейса, он сразу же запрашивает всех своих дочерних элементов. Поэтому я не могу использовать его для чего-то вроде браузера файловой системы.
Есть ли способ сделать его ленивым, чтобы он запрашивал только
children
когда элемент пользовательского интерфейса раскрывается?
class Thing: Identifiable {
let id: UUID
let depth: Int
let name: String
init(_ name: String, depth: Int = 0) {
self.id = UUID()
self.name = name
self.depth = depth
}
/// Lazy computed property
var children: [Thing]? {
if depth >= 5 { return nil }
if _children == nil {
print("Computing children property, name=\(name), depth=\(depth)")
_children = (1...5).map { n in
Thing("\(name).\(n)", depth:depth+1)
}
}
return _children
}
private var _children: [Thing]? = nil
}
struct ContentView: View {
var things: [Thing] = [Thing("1"), Thing("2"), Thing("3")]
var body: some View {
List(things, children: \.children) { thing in
Text(thing.name)
}
}
}
Несмотря на то, что первоначальный пользовательский интерфейс отображает только верхние узлы:
![](/images/a324c0e5d744531335c5e089375e78a61eecc175.png)
Вы можете видеть в консоли, что она запрашивает все - на всем протяжении дерева. Это проблема производительности для больших деревьев.
...
Computing children property, name=3.4.4.1.4, depth=4
Computing children property, name=3.4.4.1.5, depth=4
Computing children property, name=3.4.4.2, depth=3
Computing children property, name=3.4.4.2.1, depth=4
Computing children property, name=3.4.4.2.2, depth=4
...
1 ответ
Я считаю, что это может быть ошибка в SwiftUI, и я надеюсь, что Apple исправит это. Тем временем вы можете использовать следующий обходной путь:
struct Node {
var id: String
var value: String
var children: [Node]?
}
struct LazyDisclosureGroup: View {
let node: Node
@State var isExpanded: Bool = false
var body: some View {
if node.children != nil {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
if isExpanded {
ForEach(node.children!, id: \.self.id) { childNode in
LazyDisclosureGroup(node: childNode)
}
}
},
label: { Text(node.value) })
} else {
Text(node.value)
}
}
}
struct ContentView: View {
let node = Node(
id: "a",
value: "a",
children: [Node(id: "b", value: "b", children: nil),
Node(id: "c", value: "c", children: nil)])
var body: some View {
List {
LazyDisclosureGroup(node: node)
}
}
}
Я не знаю, является ли это лучшей практикой, но обходной путь использует наблюдение, что
DisclosureGroup
«замечает», когда находится внутри
List
. Их комбинация создает одинаковую визуальную структуру и поведение.