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

Swift ForEach+onDrag&onDrop实现待办项拖拽的两个技术问题

SwiftUI待办列表拖拽功能问题解答

问题背景

开发简易待办列表应用时,遇到两个拖拽相关的技术问题,相关代码如下:

Model与ViewModel

struct TaskModel: Identifiable, Equatable {
    let id = UUID().uuidString
    let title: String
}

class TaskViewModel: ObservableObject {
    @Published var tasks = [TaskModel]()
    
    init() {
        getTasks()
    }
    
    func getTasks() {
        let newTasks = [
            TaskModel(title: "0 task"),
            TaskModel(title: "1 task"),
            TaskModel(title: "2 task"),
            TaskModel(title: "3 task"),
            TaskModel(title: "4 task"),
            TaskModel(title: "5 task"),
            TaskModel(title: "6 task"),
            TaskModel(title: "7 task"),
            TaskModel(title: "8 task"),
            TaskModel(title: "9 task"),
        ]
        tasks.append(contentsOf: newTasks)
    }
    
}

View代码

struct onDropView: View {
    @StateObject var vm = TaskViewModel()
    
    @State private var dragging: TaskModel?
    @State private var changedView: Bool = false
    
    var body: some View {
        VStack {
            Text("Hello")
            
            ForEach(vm.tasks) { task in
                TaskCellView(task: task)
                    .padding(.horizontal)
                    .background(dragging?.id == task.id && changedView ? .blue : .white)
                    .opacity(dragging?.id == task.id && changedView ? 0.01 : 1) // 此处设为0会出问题
                    .onDrag {
                        self.dragging = task
                        changedView = false
                        return NSItemProvider(object: String(task.id) as NSString)
                    }
                    .onDrop(of: [UTType.text], delegate: DragDelegate(task: task,
                                                                      tasks: $vm.tasks,
                                                                      draggedTask: $dragging,
                                                                      changeView: $changedView))
            }
        }
    }
}

委托函数代码

struct DragDelegate: DropDelegate {
    let task: TaskModel
    
    @Binding var tasks: [TaskModel]
    @Binding var draggedTask: TaskModel?
    @Binding var changeView: Bool
    
    func dropEntered(info: DropInfo) {
        if draggedTask == nil { draggedTask = task }
        
        changeView = true
        
        if task != draggedTask {
            guard
                let current = draggedTask,
                let from = tasks.firstIndex(of: current),
                let to = tasks.firstIndex(of: task)
            else { return }
            
            if tasks[to].id != current.id {
                tasks.move(fromOffsets: IndexSet(integer: from), toOffset: (to > from) ? to + 1 : to )
            }
        }
        
    }
    
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
    
    func performDrop(info: DropInfo) -> Bool {
        self.changeView = false
        self.draggedTask = nil
        return true
    }
    
    
}

问题1:opacity设为0时拖拽后任务消失,设为0.01则正常

原因

当视图opacity被设为0时,SwiftUI会触发渲染优化,直接将该视图从渲染树中移除。此时被拖拽的任务视图不再存在,关联的draggedTask、changedView状态更新无法正确触发视图恢复逻辑,最终导致任务“消失”。而设为0.01时,视图仍保留在渲染树中(只是视觉上几乎不可见),状态逻辑能正常运行。

修复方案

放弃用opacity隐藏拖拽中的任务,改用hidden()修饰符或极小缩放效果,确保视图始终存在于渲染树中:

// 方案1:用hidden完全隐藏视图但保留渲染节点
.hidden(dragging?.id == task.id && changedView)

// 方案2:用极小缩放实现视觉隐藏,支持过渡动画
.scaleEffect(dragging?.id == task.id && changedView ? 0.001 : 1)
.animation(.easeInOut, value: dragging?.id == task.id && changedView)

问题2:拖拽过程中任务尺寸缩小,希望保持原尺寸

原因

默认onDrag生成的拖拽预览是原视图的简化快照,没有完整保留原视图的布局参数(比如padding、容器宽度等),导致视觉上尺寸缩小。

修复方案

自定义拖拽预览视图,确保预览与原视图尺寸完全一致。分两种实现方式:

方式1(兼容iOS 15及以下):自定义NSItemProvider预览

.onDrag {
    self.dragging = task
    changedView = false
    let provider = NSItemProvider(object: String(task.id) as NSString)
    
    // 注册自定义预览生成逻辑
    provider.registerObject(UIImage(), visibility: .all) { completion in
        // 生成和原视图完全一致的预览内容
        let previewView = TaskCellView(task: task)
            .padding(.horizontal)
            .background(.white)
            .frame(width: UIScreen.main.bounds.width - 32) // 匹配原视图宽度
        
        let renderer = ImageRenderer(content: previewView)
        renderer.scale = UIScreen.main.scale
        completion(renderer.uiImage, nil)
        return nil
    }
    return provider
}

方式2(iOS 16+):用dragPreview直接指定预览视图

.onDrag {
    self.dragging = task
    changedView = false
    return NSItemProvider(object: String(task.id) as NSString)
}
.dragPreview {
    // 直接复用原视图布局作为预览
    TaskCellView(task: dragging!)
        .padding(.horizontal)
        .background(.white)
        .frame(width: UIScreen.main.bounds.width - 32)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 12:25:58