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 } }
原因分析
- LazyVGrid的视图复用与重渲染:LazyVGrid会懒加载并复用单元格视图,当
draggedItem这个@State变量被修改时,会触发整个ContentView的重渲染。此时,之前被拖拽的单元格可能被重新初始化或复用,导致onDrag闭包再次执行。 - NSItemProvider的后续访问:拖拽操作完成后,系统可能会再次访问
NSItemProvider对象,触发onDrag闭包的二次执行。 - 状态管理逻辑缺失:原代码中没有在拖放完成后正确重置
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 }
修复说明
isDragging状态标记:确保onDrag中的状态修改逻辑仅在拖拽启动时执行一次,后续闭包调用直接返回NSItemProvider,不修改状态。- DropDelegate绑定状态:通过
@Binding让DropDelegate能够直接修改draggedItem,在拖放完成后将其重置为nil。 - 监听状态变化重置标记:在
onChange中监听draggedItem变为nil时,重置isDragging,确保下一次拖拽能正常触发逻辑。
内容的提问来源于stack exchange,提问作者Mischa
相关产品推荐
相关产品推荐

