Как сделать SwiftUI List / OutlineGroup ленивым для использования с большими деревьями, такими как файловая система?

#macos #swiftui #swiftui-list #swiftui-outlinegroup

#macos #swiftui #swiftui-list #swiftui-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)
        }
    }
}
  

Несмотря на то, что начальный пользовательский интерфейс отображает только верхние узлы:

Вы можете видеть в консоли, что он запрашивает все — вплоть до дерева. Это проблема производительности для больших деревьев.

 ...
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. Отлично работает с iOS 14, так что, похоже, это проблема macOS.

2. Другие наблюдали это в macOS, как показано на developer.apple.com/forums/thread/662937 (FB8782243). Это делает OutlineGroup бесполезным для какой-либо глубокой или ленивой иерархии.

Ответ №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 . Комбинация этих двух создает одинаковую визуальную структуру и поведение.

Комментарии:

1. Привет, Skimble! Я надеялся на ваш подход здесь и попробовал его. Я обнаружил, что, похоже, он часто работает не очень хорошо. Если я открою дерево, затем закрою и открою узел высокого уровня, многие дочерние узлы больше не откроются. Думая, что состояние может быть уничтожено при закрытии узлов, я попытался переместить isExpanded состояние в узел как @Published и соответствовало Node: ObservedObject , но это не решило проблему.