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

SwiftUI中onDrag重复调用及拖拽预览二次显示问题求助

SwiftUI拖拽问题解决:避免onDrag重复调用+立即显示自定义预览

问题分析

  1. onDrag重复调用:SwiftUI的onDrag闭包会在长按触发时和实际拖动开始时各调用一次,导致触觉反馈、ViewModel逻辑等重复执行。
  2. 预览显示两次:系统默认会在长按后显示缩放的视图预览,拖动时才显示preview参数定义的自定义视图,造成两次预览的冲突。

解决方案一:SwiftUI原生状态控制

通过状态变量标记拖拽状态,过滤重复执行逻辑;同时自定义手势控制预览显示,替换系统默认预览。

代码实现

// 添加状态变量管理拖拽和预览状态
@State private var isDragging = false
@State private var showCustomPreview = false
@State private var previewTranslation: CGPoint = .zero
@State private var currentDraggedItem: YourItemType? // 替换为你的数据类型

var body: some View {
    ZStack {
        ScrollView(.horizontal) {
            LazyVGrid(columns: ...) {
                ForEach(...) { item in
                    createItem(item: item)
                        .onTapGesture {
                            viewModel.didSelect(item)
                        }
                        .onDrag {
                            // 仅首次拖拽时执行初始化逻辑
                            guard !isDragging else {
                                return NSItemProvider(object: String(item.id) as NSString)
                            }
                            isDragging = true
                            currentDraggedItem = item
                            
                            // 一次性执行的逻辑
                            let haptic = UIImpactFeedbackGenerator(style: .medium)
                            haptic.prepare()
                            haptic.impactOccurred()
                            draggingItem = item
                            viewModel.didStartDragging(item)
                            
                            return NSItemProvider(object: String(item.id) as NSString)
                        }
                        .onDrop(of: [.text], delegate: DropRelocateDelegate(didFinishDragging: {
                            // 重置拖拽状态
                            isDragging = false
                            showCustomPreview = false
                            currentDraggedItem = nil
                            draggingItem = nil
                            viewModel.didFinishDragging()
                        }))
                        .gesture(
                            // 自定义长按+拖拽手势控制预览
                            LongPressGesture(minimumDuration: 0.2)
                                .onEnded { _ in
                                    guard !isDragging else { return }
                                    showCustomPreview = true
                                }
                                .sequenced(before: DragGesture())
                                .onChanged { value in
                                    switch value {
                                    case .second(true, let drag):
                                        // 跟随手指更新预览位置
                                        if let drag = drag {
                                            previewTranslation = drag.translation
                                        }
                                    case .second(false, _):
                                        // 拖拽结束隐藏预览
                                        showCustomPreview = false
                                    default:
                                        break
                                    }
                                }
                        )
                }
            }
        }
        .frame(height: 100)
        
        // 自定义预览视图,长按后立即显示
        if showCustomPreview, let item = currentDraggedItem {
            makePreviewItem(item: item)
                .offset(x: previewTranslation.x, y: previewTranslation.y - 50) // 调整偏移适配手指位置
                .transition(.scale)
        }
    }
}

原理说明

  • isDragging状态变量确保初始化逻辑仅执行一次,后续onDrag调用直接返回ItemProvider。
  • 通过LongPressGesture结合DragGesture手动控制自定义预览的显示和位置,完全替代系统默认的长按缩放预览。

解决方案二:UIKit拖拽交互替代(更彻底)

如果SwiftUI原生方案仍有兼容问题,可以直接使用UIKit的UIDragInteraction实现拖拽,完全控制触发时机和预览效果。

代码实现

首先创建一个包装视图:

struct DraggableView<Content: View>: UIViewRepresentable {
    typealias ItemType = YourItemType // 替换为你的数据类型
    
    let content: Content
    let item: ItemType
    let onDragStart: (ItemType) -> Void
    let onDragEnd: () -> Void
    
    func makeUIView(context: Context) -> UIView {
        // 将SwiftUI视图包装为UIView
        let hostingView = UIHostingController(rootView: content).view!
        hostingView.isUserInteractionEnabled = true
        
        // 添加拖拽交互
        let dragInteraction = UIDragInteraction(delegate: context.coordinator)
        hostingView.addInteraction(dragInteraction)
        
        return hostingView
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // 拖拽交互代理
    class Coordinator: NSObject, UIDragInteractionDelegate {
        let parent: DraggableView
        
        init(_ parent: DraggableView) {
            self.parent = parent
        }
        
        // 开始拖拽时触发,仅执行一次
        func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
            parent.onDragStart(parent.item)
            let itemProvider = NSItemProvider(object: String(parent.item.id) as NSString)
            return [UIDragItem(itemProvider: itemProvider)]
        }
        
        // 自定义长按抬起时的预览视图
        func dragInteraction(_ interaction: UIDragInteraction, previewForLifting item: UIDragItem, session: UIDragSession) -> UITargetedDragPreview? {
            let previewView = UIHostingController(rootView: parent.makePreviewItem(item: parent.item)).view!
            previewView.frame = CGRect(x: 0, y: 0, width: 100, height: 80) // 自定义预览大小
            
            // 设置预览位置为手指当前位置
            let target = UIDragPreviewTarget(
                container: UIApplication.shared.keyWindow!,
                center: session.location(in: UIApplication.shared.keyWindow!)
            )
            return UITargetedDragPreview(view: previewView, parameters: UIDragPreviewParameters(), target: target)
        }
        
        // 拖拽结束时触发
        func dragInteraction(_ interaction: UIDragInteraction, session: UIDragSession, willEndWith operation: UIDropOperation) {
            parent.onDragEnd()
        }
    }
}

然后在原视图中使用:

ScrollView(.horizontal) {
    LazyVGrid(columns: ...) {
        ForEach(...) { item in
            DraggableView(
                content: createItem(item: item),
                item: item,
                onDragStart: { item in
                    // 仅执行一次的初始化逻辑
                    let haptic = UIImpactFeedbackGenerator(style: .medium)
                    haptic.prepare()
                    haptic.impactOccurred()
                    draggingItem = item
                    viewModel.didStartDragging(item)
                },
                onDragEnd: {
                    draggingItem = nil
                    viewModel.didFinishDragging()
                }
            )
            .onTapGesture {
                viewModel.didSelect(item)
            }
            .onDrop(of: [.text], delegate: DropRelocateDelegate(didFinishDragging: {
                draggingItem = nil
                viewModel.didFinishDragging()
            }))
        }
    }
}
.frame(height: 100)

原理说明

  • 直接使用UIKit的拖拽交互,完全控制拖拽触发时机,避免SwiftUIonDrag的重复调用问题。
  • 通过previewForLifting方法自定义长按抬起时的预览视图,直接替换系统默认的缩放预览,实现长按后立即显示自定义预览的效果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 03:55:36