SwiftUI:如何实现嵌套ForEach列表的跨层级拖拽重排
实现SwiftUI层级List跨层级拖拽移动功能
要解决跨层级拖拽问题,原生onMove无法满足需求,需要通过自定义DragGesture和DropDelegate来处理不同层级间的数据转移——核心逻辑是从原父节点移除目标项,再添加到目标父节点的指定位置。以下是适配macOS、Xcode14.1+Swift5的完整实现方案:
1. 定义引用类型的数据模型
必须使用class而非struct,确保拖拽过程中修改的是同一个实例,避免值类型复制导致的状态不同步:
class FolderItem: Identifiable { let id = UUID() var name: String var children: [FolderItem] init(name: String, children: [FolderItem] = []) { self.name = name self.children = children } // 辅助方法:检查当前节点是否是目标节点的子节点(防止循环引用) func isDescendant(of target: FolderItem) -> Bool { for child in target.children { if child.id == self.id { return true } if self.isDescendant(of: child) { return true } } return false } }
2. 递归层级列表视图
实现递归ForEach构建列表,同时添加拖拽手势和自定义Drop代理:
struct HierarchicalListView: View { @Binding var items: [FolderItem] @State private var draggedItem: FolderItem? @State private var originalParent: FolderItem? @State private var originalIndex: Int? var body: some View { List { ForEach($items) { $item in DisclosureGroup(item.name) { HierarchicalListView( items: $item.children, draggedItem: $draggedItem, originalParent: $originalParent, originalIndex: $originalIndex ) } .onDrag { // 记录拖拽源的关键信息:被拖拽项、原父节点、原索引 draggedItem = item originalParent = findParent(for: item, in: items) originalIndex = items.firstIndex(where: { $0.id == item.id }) return NSItemProvider(object: item.id.uuidString as NSString) } .onDrop(of: [.text], delegate: ItemDropDelegate( targetItem: item, currentLevelItems: $items, draggedItem: $draggedItem, originalParent: $originalParent, originalIndex: $originalIndex )) } // 给当前层级的列表空白区域添加Drop代理,支持拖拽到同级末尾 .onDrop(of: [.text], delegate: SectionDropDelegate( currentLevelItems: $items, draggedItem: $draggedItem, originalParent: $originalParent, originalIndex: $originalIndex )) } } // 递归查找节点的父节点 private func findParent(for target: FolderItem, in items: [FolderItem]) -> FolderItem? { for item in items { if item.children.contains(where: { $0.id == target.id }) { return item } if let parent = findParent(for: target, in: item.children) { return parent } } return nil } }
3. 实现Item级Drop代理(拖拽到某节点作为子项)
处理将拖拽项添加到目标节点的子列表逻辑:
class ItemDropDelegate: DropDelegate { let targetItem: FolderItem @Binding var currentLevelItems: [FolderItem] @Binding var draggedItem: FolderItem? @Binding var originalParent: FolderItem? @Binding var originalIndex: Int? init(targetItem: FolderItem, currentLevelItems: Binding<[FolderItem]>, draggedItem: Binding<FolderItem?>, originalParent: Binding<FolderItem?>, originalIndex: Binding<Int?>) { self.targetItem = targetItem _currentLevelItems = currentLevelItems _draggedItem = draggedItem _originalParent = originalParent _originalIndex = originalIndex } func performDrop(info: DropInfo) -> Bool { guard let dragged = draggedItem, let parent = originalParent, let index = originalIndex, dragged.id != targetItem.id, !dragged.isDescendant(of: targetItem) else { return false } // 从原父节点移除 parent.children.remove(at: index) // 添加到目标节点的子列表 targetItem.children.append(dragged) // 重置拖拽状态 resetDragState() return true } func proposal(for info: DropInfo) -> DropProposal? { DropProposal(operation: .move) } private func resetDragState() { draggedItem = nil originalParent = nil originalIndex = nil } }
4. 实现Section级Drop代理(拖拽到当前层级作为同级项)
处理将拖拽项添加到当前层级列表的逻辑:
class SectionDropDelegate: DropDelegate { @Binding var currentLevelItems: [FolderItem] @Binding var draggedItem: FolderItem? @Binding var originalParent: FolderItem? @Binding var originalIndex: Int? init(currentLevelItems: Binding<[FolderItem]>, draggedItem: Binding<FolderItem?>, originalParent: Binding<FolderItem?>, originalIndex: Binding<Int?>) { _currentLevelItems = currentLevelItems _draggedItem = draggedItem _originalParent = originalParent _originalIndex = originalIndex } func performDrop(info: DropInfo) -> Bool { guard let dragged = draggedItem, let parent = originalParent, let index = originalIndex else { return false } // 同层级移动直接用原生move方法 if parent.children === currentLevelItems { currentLevelItems.move(fromOffsets: IndexSet(integer: index), toOffset: currentLevelItems.count) } else { // 跨层级:从原父节点移除,添加到当前层级 parent.children.remove(at: index) currentLevelItems.append(dragged) } resetDragState() return true } func proposal(for info: DropInfo) -> DropProposal? { DropProposal(operation: .move) } private func resetDragState() { draggedItem = nil originalParent = nil originalIndex = nil } }
5. 使用示例
struct ContentView: View { @State private var rootItems: [FolderItem] = [ FolderItem(name: "user1234", children: [ FolderItem(name: "Photos", children: [ FolderItem(name: "Summer 2023") ]) ]), FolderItem(name: "newuser") ] var body: some View { HierarchicalListView(items: $rootItems) .frame(width: 350, height: 450) .navigationTitle("层级文件夹") } }
内容的提问来源于stack exchange,提问作者DaPhil
相关产品推荐
相关产品推荐

