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

SwiftUI List结合onDrag/onDrop实现项替换与重排的解决方案

SwiftUI List 同时实现拖拽替换与重排功能

问题背景

需要在SwiftUI List中实现两个拖拽交互功能:

  • 项替换:拖拽某一项到另一项上,用拖拽项替换目标项内容
  • 项重排:通过拖拽手势调整列表项的顺序

单独使用onDrag/onDrop或onMove时功能正常,但两者结合时系统重排逻辑会覆盖自定义拖拽行为,需要调整交互逻辑实现共存。

完整解决方案代码

import SwiftUI

struct ContentView: View {
    @State private var items = ["Item 1", "Item 2", "Item 3", "Item 4"]
    @State private var highlightedIndex: Int?
    @State private var isReplaceMode = false
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(items.indices, id: \.self) { index in
                    HStack {
                        Text(items[index])
                            .foregroundColor(highlightedIndex == index ? .red : .primary)
                        Spacer()
                    }
                    .onDrag {
                        let dragData = DragData(index: index, isReplace: true)
                        return NSItemProvider(object: try! JSONEncoder().encode(dragData) as NSData)
                    }
                    .onDrop(of: [.customDragData], delegate: ItemDropDelegate(
                        itemIndex: index,
                        items: $items,
                        highlightedIndex: $highlightedIndex,
                        isReplaceMode: $isReplaceMode
                    ))
                }
                .onMove(perform: moveItems)
            }
            .navigationTitle("拖拽操作演示")
            .toolbar {
                EditButton()
            }
        }
    }
    
    private func moveItems(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
}

struct DragData: Codable {
    let index: Int
    let isReplace: Bool
}

struct ItemDropDelegate: DropDelegate {
    let itemIndex: Int
    @Binding var items: [String]
    @Binding var highlightedIndex: Int?
    @Binding var isReplaceMode: Bool
    
    func performDrop(info: DropInfo) -> Bool {
        guard let itemProvider = info.itemProviders(for: [.customDragData]).first else {
            return false
        }
        
        itemProvider.loadObject(ofClass: NSData.self) { data, _ in
            guard let data = data as? Data,
                  let dragData = try? JSONDecoder().decode(DragData.self, from: data) else {
                return
            }
            
            let sourceIndex = dragData.index
            guard sourceIndex != itemIndex else { return }
            
            if isReplaceMode {
                let draggedItem = items[sourceIndex]
                items[itemIndex] = draggedItem
            }
        }
        
        isReplaceMode = false
        highlightedIndex = nil
        return true
    }
    
    func validateDrop(info: DropInfo) -> Bool {
        let isOverItem = info.location.y < (info.itemFrame?.maxY ?? 0) - 20
        isReplaceMode = isOverItem
        highlightedIndex = isOverItem ? itemIndex : nil
        return info.hasItemsConforming(to: [.customDragData])
    }
    
    func dropEntered(info: DropInfo) {
        let isOverItem = info.location.y < (info.itemFrame?.maxY ?? 0) - 20
        isReplaceMode = isOverItem
        highlightedIndex = isOverItem ? itemIndex : nil
    }
    
    func dropExited(info: DropInfo) {
        highlightedIndex = nil
        isReplaceMode = false
    }
    
    func proposal(for info: DropInfo) -> DropProposal? {
        let isOverItem = info.location.y < (info.itemFrame?.maxY ?? 0) - 20
        return DropProposal(operation: isOverItem ? .copy : .move)
    }
}

extension UTType {
    static let customDragData = UTType(exportedAs: "com.example.customDragData")
}

核心实现思路

  1. 自定义拖拽数据类型

    • 创建DragData结构体封装拖拽索引和操作标记,同时定义专属UTType,避免与系统拖拽类型冲突,确保自定义逻辑不被系统重排覆盖。
  2. 区分拖拽交互场景

    • 通过DropInfo中的位置信息判断拖拽是覆盖在列表项上还是处于项间隙:
      • 覆盖项上时激活替换模式,高亮目标项,释放后执行替换
      • 处于间隙时触发系统默认重排逻辑,显示重排指示器
  3. 兼容系统重排

    • 保留ForEach的.onMove(perform:)方法启用系统拖拽重排,通过DropProposal返回不同操作类型(.copy对应替换,.move对应重排),让系统正确区分两种操作行为。
  4. 状态控制

    • 用isReplaceMode标记当前操作类型,避免重排与替换逻辑冲突;highlightedIndex提供视觉反馈,明确用户当前操作的目标项。

操作说明

  • 替换操作:拖拽列表项到另一项的上半区域,目标项变红后释放,完成内容替换
  • 重排操作:拖拽列表项到两项之间的间隙,出现系统重排指示器后释放,完成顺序调整

内容的提问来源于stack exchange,提问作者János

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 03:32:11