如何可靠检测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
相关产品推荐
相关产品推荐

