You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 17:40:18