我正在考虑在
iOS
中创建树数据结构的某种可视化表示。树中节点保存的数据是图像和标签,一个节点最多可以有 6 个子节点。
目前,我有一个带有自定义布局的集合视图,当我遍历自制树时,我以编程方式计算每个节点的 x 和 y。
这个解决方案有效,但只是勉强有效。当我构建更多功能时,我预计它会崩溃。
我考虑过在树构建完成后制作图像并仅使用图像视图,但我计划在分支上实现某种展开/折叠。我还需要一种放大和缩小整个树的方法,这对于集合视图来说似乎并不容易。
有更好的解决方案吗?
在 UIScrollView 中只使用简单的视图怎么样?
这样你就可以:
这里我使用
UIView
创建了一个示例项目:
https://github.com/crisisGriega/swift-simple-tree-drawer
这是一个快速的开发,所以有很多东西可以改进,比如节点之间的线(连接器)的绘制方式。此外,在此示例中,节点被添加到
UIView
而不是 UIScrollView
。但您可以点击节点来显示/隐藏其子节点。
考虑使用
SpriteKit
而不是使用 UIKit
组件。使用 UICollectionView
创建动态树状布局并不容易,因为您可以查看它的数据源定义,它不是为了建模树数据,而是为平面列表数据建模。如果数据模型根本不同,一切就变得很难进行。
使用
SpriteKit
,每个节点对象都可以由 SKSpriteNode
对象渲染。子节点的布局由其父节点管理。您还可以使用物理引擎自动定位节点,它还有一个额外的好处,即可以用最小的 efferot 避免重叠。最后但并非最不重要的一点是,SpriteKit
开箱即用地支持缩放和滚动。
假设一个示例模型:
import Foundation
struct TreeNode: Hashable, Sendable {
let id = UUID()
let name: String
var children: [TreeNode]?
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: TreeNode, rhs: TreeNode) -> Bool {
return lhs.id == rhs.id
}
}
enum TreeSection: CaseIterable {
case main
}
从 iOS 13.0 开始,可以轻松完成
import UIKit
final class TreeListController: UIViewController {
typealias Item = TreeNode
typealias Section = TreeSection
typealias DS = UICollectionViewDiffableDataSource<Section, Item>
typealias DSSnapshot = NSDiffableDataSourceSnapshot<Section, Item>
typealias CellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item>
private let items: [Item]
private let collectionView: UICollectionView
private let dataSource: DS
init(treeNodes: [Item]) {
let collectionView = Self.createCollectionView()
let dataSource = Self.createDataSource(with: collectionView)
self.items = treeNodes
self.collectionView = collectionView
self.dataSource = dataSource
collectionView.dataSource = dataSource
super.init(nibName: nil, bundle: nil)
}
@available(iOS, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = collectionView
}
override func viewDidLoad() {
super.viewDidLoad()
applySnapshot(treeNodes: items, to: .main)
}
private static func createCollectionView() -> UICollectionView {
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
return UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
}
private static func createDataSource(with collectionView: UICollectionView) -> DS {
let cellRegistration: CellRegistration = {
CellRegistration { cell, indexPath, treeNode in
var config = cell.defaultContentConfiguration()
config.image = UIImage(systemName: "folder")
config.imageProperties.tintColor = .systemBlue
config.text = treeNode.name
cell.contentConfiguration = config
// include disclosure indicator for nodes with children
cell.accessories = treeNode.children != nil ? [.outlineDisclosure()] : []
}
}()
return DS(collectionView: collectionView) { collectionView, indexPath, treeNode -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: treeNode)
}
}
private func applySnapshot(treeNodes: [TreeNode], to section: Section) {
// reset section
var snapshot = DSSnapshot()
snapshot.appendSections([section])
dataSource.apply(snapshot, animatingDifferences: false)
// initial snapshot with the root nodes
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<TreeNode>()
sectionSnapshot.append(treeNodes)
func addItemsRecursively(_ nodes: [TreeNode], to parent: TreeNode?) {
nodes.forEach { node in
// for each node we add its children, then recurse into the children nodes
if let children = node.children, !children.isEmpty {
sectionSnapshot.append(children, to: node)
addItemsRecursively(children, to: node)
}
// show every node expanded
sectionSnapshot.expand([node])
}
}
addItemsRecursively(treeNodes, to: nil)
dataSource.apply(sectionSnapshot, to: section, animatingDifferences: true)
}
}
示例内容:
let sampleTreeNodes = [TreeNode(name: "Folder 1", children: [
TreeNode(name: "Subfolder 1-1"),
TreeNode(name: "Subfolder 1-2", children: [
TreeNode(name: "File 1"),
TreeNode(name: "File 2")
])
])]
SwiftUI 的额外包装:
import SwiftUI
import SwiftData
struct TreeListRepresentableView: UIViewControllerRepresentable {
typealias UIViewControllerType = TreeListController
let treeNodes: [TreeNode]
func makeUIViewController(context: Context) -> TreeListController {
TreeListController(treeNodes: createSampleFolderItems())
}
func updateUIViewController(_ uiViewController: TreeListController, context: Context) {
}
func createSampleFolderItems() -> [TreeNode] {
treeNodes
}
}
#Preview {
TreeListRepresentableView(treeNodes: sampleTreeNodes)
}
尝试了大约 50 个文件夹,看起来不错: