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

SwiftUI中draggable与dropDestination拖拽网格单元格动画失效问题

SwiftUI网格单元格拖拽移动:过渡动画失效问题

我在SwiftUI中结合draggable与dropDestination实现网格单元格的拖拽移动功能,目前功能逻辑正常,但拖拽过程中的过渡动画完全不生效。

相关代码

struct PhotosGridViewDraggable: View {
    
    @State var draggedItem: Color?
    
    @State var colors: [Color] = [.red, .blue, .gray, .green, .pink, .yellow]
    
    @Namespace var animation
    
    var body: some View {
        GeometryReader { proxy in
            let hGap: CGFloat = 14
            let width = proxy.size.width - (hGap * 2)
            let cellWidth = (width / 3)
            let cellSize = CGSize(width: cellWidth, height: cellWidth * 1.5)
            
            let columns = [GridItem(.fixed(cellWidth)), GridItem(.fixed(cellWidth)), GridItem(.fixed(cellWidth))]
            
            LazyVGrid(columns: columns) {
                ForEach(colors.indices, id: \.self) { index in
                    let color = colors[index]
                    Rectangle()
                        .fill(color)
                        .frame(width: cellSize.width, height: cellSize.height)
                        .draggable(color, preview: {
                            RoundedRectangle(cornerRadius: 10)
                                .fill(.ultraThinMaterial)
                                .frame(width: 1, height: 1)
                                .onAppear {
                                    draggedItem = color
                                }
                        })
                        .dropDestination(for: Color.self, action: { items, location in
                            return false
                        }, isTargeted: { status in
                            if let draggedItem = draggedItem, status, draggedItem != color {
                                if let sourceIndex = colors.firstIndex(of: draggedItem), let destinationIndex = colors.firstIndex(of: color) {
                                    withAnimation { // 此处动画无效果
                                        let sourceItem = colors.remove(at: sourceIndex)
                                        colors.insert(sourceItem, at: destinationIndex)
                                    }
                                }
                            }
                            
                        })
                }
            }
            
        }
    }
    
}

效果对比

  • 预期效果:拖拽单元格时,其他单元格会平滑过渡到新位置,形成连贯的动画。
  • 实际效果:拖拽完成后,单元格直接跳转到目标位置,没有任何过渡动画。

解决方案

问题根源主要有两点:

  1. 使用ForEach(colors.indices, id: \.self)绑定索引,当数组元素移动时,SwiftUI无法正确识别视图的身份,导致动画无法触发。
  2. 在dropDestination的isTargeted闭包中修改数据源,这个闭包的触发时机不适合处理数据变更,且Color作为值类型,用firstIndex(of:)存在潜在的重复元素识别问题。

修改步骤:

  1. 创建带唯一标识的数据结构体,替代直接使用Color。
  2. 将数据源改为带唯一ID的数组类型,确保每个元素有稳定的身份标识。
  3. 修改ForEach绑定元素的id,让SwiftUI能跟踪每个视图的身份。
  4. 在dropDestination的action闭包中处理数据交换,配合withAnimation实现平滑过渡。
  5. 优化拖拽预览样式,提升交互体验。

修改后的完整代码

struct PhotosGridViewDraggable: View {
    
    @State private var draggedItem: GridColor?
    @State private var colors: [GridColor] = [
        .init(color: .red),
        .init(color: .blue),
        .init(color: .gray),
        .init(color: .green),
        .init(color: .pink),
        .init(color: .yellow)
    ]
    
    var body: some View {
        GeometryReader { proxy in
            let hGap: CGFloat = 14
            let width = proxy.size.width - (hGap * 2)
            let cellWidth = width / 3
            let cellSize = CGSize(width: cellWidth, height: cellWidth * 1.5)
            
            let columns = Array(repeating: GridItem(.fixed(cellWidth)), count: 3)
            
            LazyVGrid(columns: columns, spacing: hGap) {
                ForEach($colors) { $colorItem in
                    Rectangle()
                        .fill(colorItem.color)
                        .frame(width: cellSize.width, height: cellSize.height)
                        .cornerRadius(10)
                        .draggable(colorItem) {
                            // 自定义拖拽预览
                            Rectangle()
                                .fill(colorItem.color)
                                .frame(width: cellSize.width, height: cellSize.height)
                                .cornerRadius(10)
                                .shadow(radius: 8)
                        }
                        .dropDestination(for: GridColor.self) { items, location in
                            guard let draggedItem = items.first,
                                  draggedItem.id != colorItem.id else { return false }
                            
                            withAnimation(.easeInOut) {
                                if let sourceIndex = colors.firstIndex(where: { $0.id == draggedItem.id }),
                                   let destIndex = colors.firstIndex(where: { $0.id == colorItem.id }) {
                                    let item = colors.remove(at: sourceIndex)
                                    // 处理插入位置的偏移(源索引小于目标时需要减1)
                                    colors.insert(item, at: sourceIndex < destIndex ? destIndex - 1 : destIndex)
                                }
                            }
                            return true
                        }
                }
            }
            .padding(.horizontal, hGap)
        }
    }
}

struct GridColor: Identifiable {
    let id = UUID()
    let color: Color
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 07:43:10