大纲视图 (OutlineView)

目前(截至 2022 年 7 月),在 SwiftUI 和 macOS 中实现可以懒加载子项的大纲视图没有明显的方法。请参阅此 stackoverflow 问题

本软件包包含一个 OutlineView,它可以实现这个功能。它不是最漂亮的,但如果有人有兴趣使用它,我会清理它并添加功能。

使用示例

outline_test_screenshot2

import SwiftUI
import EncodiaOutlineView

struct ContentView: View {
    
    @State var roots: [Thing] = makeOutlineRoots()
    @StateObject var selection = OutlineViewSingleSelectionModel<Thing>()
    // @StateObject var selection = OutlineViewMultiSelectionModel<Thing>()
    @State var expansion: Set<UUID> = []
    
    var body: some View {
        HStack {
            ScrollView {
                // OutlineView(roots: roots, expansion: $expansion) { node in
                OutlineView(roots: roots, selection: selection, expansion: $expansion) { node in
                    Text(node.name)
                }
            }
            .frame(width: 200)
            Divider()
            Text("Some detail view would go here.")
                .frame(maxWidth: .infinity)
        }
    }
}

@MainActor func makeOutlineRoots() -> [Thing] {
    var roots = [Thing]()
    for i in 1...5 {
        let t = Thing(name: "Thing \(i)")
        roots.append( t )
    }
    return roots
}

final class Thing: TreeNodeProtocol {
    let id: UUID
    let depth: Int
    let name: String
    let color: Color
    init(name: String, depth: Int = 0) {
        self.id = UUID()
        self.name = name
        self.depth = depth
        self.color = Color(hue: Double.random(in: 0...1), saturation: 0.7, brightness: 0.7, opacity: 0.5)
    }
    
    var canHaveChildren: Bool { depth < 5 }
    var isLoadingChildren: Bool { false }
    
    /// Lazy computed property
    var children: [Thing] {
        if depth >= 5 { return [] }
        if _children == nil {
            print("Computing children property, name=\(name), depth=\(depth)")
            _children = (1...5).map { n in
                Thing(name: "\(name).\(n)", depth:depth+1)
            }
        }
        return _children!
    }
    private var _children: [Thing]? = nil
}

这是另一个使用懒加载来查看文件系统的示例。如果您在 Xcode 中尝试此示例,您可能需要在目标的“Signing and Capabilities”选项卡下禁用沙盒。

struct FileSystemView: View {
    
    var fileSystemRoots = [FileSystemNode(url: URL(fileURLWithPath: "/"))]
    @StateObject var selection = OutlineViewSingleSelectionModel<FileSystemNode>()
    // @StateObject var selection = OutlineViewMultiSelectionModel<Thing>()
    @State var expansion: Set<URL> = []
    @State var detailText: String = "??"
    
    var body: some View {
        HStack(spacing: 0) {
            ScrollView {
                // OutlineView(roots: roots, expansion: $expansion) { node in
                OutlineView(roots: fileSystemRoots, selection: selection, expansion: $expansion, onNodeClick: onNodeClick) { node in
                    Text(node.name)
                }
            }
            .frame(width: 350)
            Divider()
            Text("Some detail view for \(detailText) would go here.")
                .frame(maxWidth: .infinity)
        }
    }
    
    func onNodeClick(_ node: FileSystemNode) {
        detailText = node.url.description
    }
}

final class FileSystemNode: TreeNodeProtocol {
    let url: URL
    
    init(url: URL) {
        self.url = url
    }
    
    // Needs to implement Identifiable.
    var id: URL { url }
    
    var name: String {
        url.lastPathComponent
    }

    var isDirectory: Bool {
        // Is there a better `isDirectory` check in Swift these days?
        var isDir: ObjCBool = false
        if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir) {
            return isDir.boolValue
        }
        else {
            return false
        }
    }
    
    var canHaveChildren: Bool { isDirectory }

    @Published var isLoadingChildren: Bool = false
    
    /// Lazy computed property
    var children: [FileSystemNode] {
        if let cached = _children {
            return cached
        } else {
            if !isLoadingChildren {
                isLoadingChildren = true
                DispatchQueue.global(qos: .userInitiated).async { [self] in
                    let childNodes = loadChildNodes()
                    DispatchQueue.main.async { [self] in
                        _children = childNodes
                        isLoadingChildren = false
                    }
                }
            }
            return []
        }
    }
    
    private func loadChildNodes() -> [FileSystemNode] {
        do {
            // Simulate something that takes longer
            Thread.sleep(forTimeInterval: 3.0)
            let urls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: [])            
            let nodes = urls.map { url in FileSystemNode(url: url)}
            return nodes
        } catch let err {
            print("Error reading directory: \(url)")
            return []
        }
    }
    
    @Published private var _children: [FileSystemNode]? = nil
}