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

如何可靠检测SwiftUI中.onDrag的取消或结束状态?

解决LazyVGrid拖放至禁止区域后draggedItem未重置的问题

你的核心问题是:当拖放操作在非Grid区域(黄色禁止区)结束时,没有触发draggedItem的重置逻辑,导致原项目保持不可见状态。以下是两种可行的解决方案:

方案一:给禁止拖放区域添加DropDelegate

直接给黄色区域配置一个专门的DropDelegate,捕获拖放进入或结束事件,强制重置draggedItem,同时还能显示禁止拖放的光标提示:

修改后的完整代码

struct DragTestView: View {
    class ViewModel: ObservableObject {
        @Published var items: [String] = Array(0..<9).map { "\($0)" }
    }
    
    @StateObject var viewModel = ViewModel()
    @State private var draggedItem: String?
    @State private var dragEndTimestamp = Date.distantPast
    
    var body: some View {
        VStack(spacing: 0) {
            LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 100)), count: 3)) {
                ForEach(viewModel.items, id: \.self) { item in
                    Text("\(item)")
                        .frame(maxWidth: .infinity, maxHeight: 100)
                        .padding()
                        .background(Color.blue)
                        .opacity(draggedItem == item ? 0.001 : 1)
                        .onDrag{
                            print("Drag \(item)")
                            guard Date().timeIntervalSince(dragEndTimestamp) >= 2 else {
                                print("Too fast")
                                return NSItemProvider()
                            }
                            
                            draggedItem = item
                            return NSItemProvider(object: item as NSString)
                        } preview: {
                            Text("\(item)")
                                .padding()
                                .background(Color.red)
                        }
                        .onDrop(of: [.text], delegate: ItemDropDelegate(item: item, draggedItem: $draggedItem, model: viewModel))
                }
            }
            .padding()
            .frame(maxHeight: .infinity)
            .background(.green)
            
            Text("No drop here")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.yellow)
                .onDrop(of: [.text], delegate: CancelDropDelegate(draggedItem: $draggedItem))
        }
        .onChange(of: draggedItem) {
            dragEndTimestamp = Date()
            print("Changed dragged item to: \(draggedItem ?? "nil")")
        }
    }
    
    struct ItemDropDelegate: DropDelegate {
        let item: String
        @Binding var draggedItem: String?
        var model: DragTestView.ViewModel
        
        func performDrop(info: DropInfo) -> Bool {
            draggedItem = nil
            return true
        }
        
        func dropUpdated(info: DropInfo) -> DropProposal? {
            return DropProposal(operation: .move)
        }
        
        func dropEntered(info: DropInfo) {
            guard let draggedItem = draggedItem, draggedItem != item,
                  let from = model.items.firstIndex(of: draggedItem),
                  let to = model.items.firstIndex(of: item) else {return}
            model.items.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to)
        }
    }
    
    struct CancelDropDelegate: DropDelegate {
        @Binding var draggedItem: String?
        
        func dropEntered(info: DropInfo) {
            draggedItem = nil
        }
        
        func performDrop(info: DropInfo) -> Bool {
            draggedItem = nil
            return false // 明确拒绝放置操作
        }
        
        func dropUpdated(info: DropInfo) -> DropProposal? {
            return DropProposal(operation: .cancel) // 显示禁止拖放的光标样式
        }
    }
}

方案二:监听全局拖放结束通知

利用AppKit的NSDragSessionEnded通知,全局监听拖放操作的结束事件,无论拖放在哪里结束,都重置draggedItem:

关键代码修改

在VStack中添加onAppear和onDisappear来监听通知:

VStack(spacing: 0) {
    // 原有Grid和黄色区域代码
}
.onAppear {
    NotificationCenter.default.addObserver(forName: .NSDragSessionEnded, object: nil, queue: .main) { _ in
        draggedItem = nil
    }
}
.onDisappear {
    NotificationCenter.default.removeObserver(self, name: .NSDragSessionEnded, object: nil)
}

关于API疑问的说明

SwiftUI的Drag/Drop API确实没有直接暴露onDropCanceled或onDropEnded这类全局回调,这是因为它的设计更偏向于区域化的拖放处理。但通过上述两种方式,完全可以覆盖所有拖放结束的场景,不需要额外手动维护复杂的临时状态追踪逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 10:33:13