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
相关产品推荐
相关产品推荐

