SwiftUI LazyVGrid拖拽动画Bug:快速拖拽至屏幕角落释放后动画错位消失
修复SwiftUI LazyVGrid拖拽动画错位问题
这个拖拽动画错位的问题我之前也遇到过,主要是matchedGeometryEffect和LazyVGrid的懒加载特性交互时,状态和动画时机没处理好。快速拖拽到屏幕角落释放时,dragging状态没有及时清理,加上全局动画的触发不够精准,就会出现动画残留错位的情况。下面是修复后的完整代码,我会逐一说明关键修改点:
import SwiftUI import UniformTypeIdentifiers struct SameSizeView: View { @Namespace private var animation @State private var dragging: MyRect? @State private var rects = [ MyRect(), MyRect(), MyRect(), MyRect(), MyRect(), MyRect(), MyRect(), MyRect(), MyRect(), MyRect() ] // 用GeometryReader获取尺寸,替代硬编码的屏幕边界 @State private var gridItemWidth: CGFloat = 0 let columns: [GridItem] = [ GridItem(.flexible(), spacing: MyRect.spacing, alignment: .topTrailing), GridItem(.flexible(), spacing: MyRect.spacing, alignment: .topLeading) ] var body: some View { GeometryReader { geo in ScrollView(.vertical, showsIndicators: false) { LazyVGrid(columns: columns, alignment: .center, spacing: MyRect.spacing) { ForEach(rects) { rect in rect .matchedGeometryEffect( id: rect.id, in: animation, isSource: dragging == rect // 明确标识拖拽源视图 ) .onDrag { dragging = rect return NSItemProvider(object: rect.id.uuidString as NSString) } .onDrop(of: [UTType.text], delegate: MyDropDelegate(item: rect, listData: $rects, current: $dragging, animation: animation)) } } .padding(.horizontal, MyRect.spacing / 2) } .onAppear { // 计算适配父视图的网格项宽度 gridItemWidth = (geo.size.width - MyRect.spacing * 2) / 2 } } } } struct MyRect: View, Identifiable, Equatable { var id = UUID() @State var color = Color.init(.sRGB, red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1), opacity: 1) static var spacing: CGFloat = 16 var body: some View { Rectangle() .fill(color) .frame( width: UIScreen.main.bounds.width / 2 - MyRect.spacing * 1.5, height: 2 * 62 + MyRect.spacing ) } static func == (lhs: MyRect, rhs: MyRect) -> Bool { return lhs.id == rhs.id } } struct MyDropDelegate: DropDelegate { let item: MyRect @Binding var listData: [MyRect] @Binding var current: MyRect? let animation: Namespace.ID // 传入命名空间确保动画同步 func dropEntered(info: DropInfo) { guard let currentItem = current, currentItem != item else { return } let from = listData.firstIndex(of: currentItem)! let to = listData.firstIndex(of: item)! if listData[to] != currentItem { // 用withAnimation包裹数据移动,确保动画与数据变更同步 withAnimation(.default) { listData.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to) } UIImpactFeedbackGenerator(style: .light).impactOccurred() } } func dropUpdated(info: DropInfo) -> DropProposal? { return DropProposal(operation: .move) } func performDrop(info: DropInfo) -> Bool { // 拖拽结束时用动画同步重置状态 withAnimation(.default) { current = nil } return true } // 处理拖拽到屏幕外的边界情况,避免状态残留 func dropExited(info: DropInfo) { let screenBounds = UIScreen.main.bounds if info.location.y < 0 || info.location.y > screenBounds.height || info.location.x < 0 || info.location.x > screenBounds.width { withAnimation(.default) { current = nil } } } }
关键修改说明
- 精准匹配拖拽源视图:给
matchedGeometryEffect添加isSource: dragging == rect参数,明确告诉系统当前哪个视图是拖拽的源,避免LazyVGrid中未渲染的视图干扰动画匹配逻辑。 - 同步动画与数据变更:把原来的全局动画替换为在数据移动、状态重置时用
withAnimation包裹,确保动画和数据状态完全同步,不会出现动画滞后或错位。 - 处理边界拖拽场景:新增
dropExited方法,当拖拽操作移出屏幕范围时主动重置dragging状态,避免因为未触发performDrop导致的动画残留。 - 优化布局适配性:改用
GeometryReader获取父视图尺寸,减少对UIScreen.main.bounds的硬编码依赖,让布局在不同设备上更稳定。
内容的提问来源于stack exchange,提问作者Mkschiller




