You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

SwiftUI LazyVGrid拖拽动画Bug:快速拖拽至屏幕角落释放后动画错位消失

修复SwiftUI LazyVGrid拖拽动画错位问题

这个拖拽动画错位的问题我之前也遇到过,主要是matchedGeometryEffectLazyVGrid的懒加载特性交互时,状态和动画时机没处理好。快速拖拽到屏幕角落释放时,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

火山引擎 最新活动