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

SwiftUI中onDrag闭包拖放完成后二次调用的原因及解决方法

SwiftUI LazyVGrid中onDrag二次调用问题的原因与修复

问题描述

在SwiftUI的LazyVGrid中实现拖放功能时,发现onDrag闭包会被调用两次:第一次是用户开始拖拽时,第二次在拖放完成约1秒后。这导致draggedItem始终被赋值,无法重置为nil。简化后的代码如下:

视图代码

struct ContentView: View {
    
    @State var items: [ItemModel] = (0..<30).map { .init(position: $0, id: $0) }
    @State var draggedItem: ItemModel?
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [.init(.fixed(100)), .init(.fixed(100)), .init(.fixed(100))]) {
                ForEach(items) { item in
                    print("item", item.id)
                    return ZStack {
                        RoundedRectangle(cornerRadius: 8)
                            .frame(maxWidth: .infinity)
                            .foregroundStyle(.mint)
                        Text("\(item.position)")
                            .padding()
                    }
                    .onDrop(of: [.plainText], delegate: ItemDropDelegate())
                    .onDrag {
                        // 第二次调用发生在拖放完成后
                        print("––– onDrag –––", item.id)
                        draggedItem = item
                        return NSItemProvider(object: String(item.id) as NSString)
                    }
                }
            }
        }
        .onChange(of: draggedItem) { oldValue, newValue in
            print("draggedItem:", newValue!.id)
        }
    }
}

模型与DropDelegate代码

struct ItemModel: Identifiable, Hashable {
    var position: Int
    var id: Int
}

struct ItemDropDelegate: DropDelegate {
    func performDrop(info: DropInfo) -> Bool {
        print("Drop performed")
        return false
    }
}

原因分析

  1. LazyVGrid的视图复用与重渲染:LazyVGrid会懒加载并复用单元格视图,当draggedItem这个@State变量被修改时,会触发整个ContentView的重渲染。此时,之前被拖拽的单元格可能被重新初始化或复用,导致onDrag闭包再次执行。
  2. NSItemProvider的后续访问:拖拽操作完成后,系统可能会再次访问NSItemProvider对象,触发onDrag闭包的二次执行。
  3. 状态管理逻辑缺失:原代码中没有在拖放完成后正确重置draggedItem,且onDrag中直接修改@State变量会加剧重渲染循环。

修复方案

关键修复点

  • 添加拖拽状态标记,避免onDrag闭包重复执行核心逻辑
  • 在DropDelegate中通过Binding重置draggedItem
  • 解耦状态修改与视图重渲染的循环

修改后的完整代码

struct ContentView: View {
    @State var items: [ItemModel] = (0..<30).map { .init(position: $0, id: $0) }
    @State var draggedItem: ItemModel?
    @State private var isDragging = false // 标记是否正在拖拽

    var body: some View {
        ScrollView {
            LazyVGrid(columns: [.init(.fixed(100)), .init(.fixed(100)), .init(.fixed(100))]) {
                ForEach(items) { item in
                    ZStack {
                        RoundedRectangle(cornerRadius: 8)
                            .frame(maxWidth: .infinity)
                            .foregroundStyle(.mint)
                        Text("\(item.position)")
                            .padding()
                    }
                    .onDrop(of: [.plainText], delegate: ItemDropDelegate(draggedItem: $draggedItem))
                    .onDrag {
                        // 仅在拖拽开始时执行状态修改逻辑
                        guard !isDragging else {
                            return NSItemProvider(object: String(item.id) as NSString)
                        }
                        print("––– onDrag –––", item.id)
                        isDragging = true
                        draggedItem = item
                        return NSItemProvider(object: String(item.id) as NSString)
                    }
                }
            }
        }
        .onChange(of: draggedItem) { oldValue, newValue in
            // 拖拽完成后重置拖拽状态
            if newValue == nil {
                isDragging = false
            }
            print("draggedItem:", newValue?.id ?? "nil")
        }
    }
}

// 修改DropDelegate,通过Binding重置draggedItem
struct ItemDropDelegate: DropDelegate {
    @Binding var draggedItem: ItemModel?

    func performDrop(info: DropInfo) -> Bool {
        print("Drop performed")
        draggedItem = nil // 拖放完成后清空拖拽标记
        return false
    }
}

struct ItemModel: Identifiable, Hashable {
    var position: Int
    var id: Int
}

修复说明

  1. isDragging状态标记:确保onDrag中的状态修改逻辑仅在拖拽启动时执行一次,后续闭包调用直接返回NSItemProvider,不修改状态。
  2. DropDelegate绑定状态:通过@Binding让DropDelegate能够直接修改draggedItem,在拖放完成后将其重置为nil。
  3. 监听状态变化重置标记:在onChange中监听draggedItem变为nil时,重置isDragging,确保下一次拖拽能正常触发逻辑。

内容的提问来源于stack exchange,提问作者Mischa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 01:57:13