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

SwiftUI 适配iOS13的自定义网格如何实现元素重排动画

iOS13环境下手动搭建SwiftUI网格的位移动画实现

实际遇到的问题

  • 开发约束:需适配iOS13,无法使用iOS14及以上版本才提供的LazyVGrid组件,当前采用嵌套双层ForEach的方式,外层遍历行索引、内层遍历列索引手动堆叠网格布局
  • 动画异常:调整行、列索引数组顺序可触发基础动画,但打乱元素数组触发重排时,元素会直接跳转到新位置,没有跨位置的平滑位移动画效果
  • 原有测试代码如下:
struct ContentView: View {
    
    @State var elements = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    @State var columns = [0, 1]
    
    @State var rows: [Int] = [0, 1, 2, 3, 4]
    
    var body: some View {
        VStack {
            ForEach(rows, id: \.self) { i in
                HStack {
                    ForEach(columns, id: \.self) { j in
                        Text("element\(elements[2 * i + j])")
                    }
                }
            }
            
            Button("Randomize") {
                self.elements.shuffle()
            }
        }.animation(.default)
    }
    
}

动画失效的核心原因

原写法中,HStack、VStack负责固定排布每个网格位置,每个网格位置的容器视图身份是固定的“第i行第j列”,打乱元素数组时,容器本身的位置从来没有变过,只是容器内部的文本内容被替换。SwiftUI无法识别“同一个元素从A位置移动到B位置”的变化,只会判定为A位置的容器换了内容、B位置的容器也换了内容,因此不会触发跨位置的位移动画。

适配实现方案

  • 调整视图身份绑定逻辑:不再按行、列的位置硬算元素下标,直接把每个网格元素本身作为ForEach的遍历目标,给每个视图绑定和元素值对应的稳定身份标识,保证同一个元素不管怎么移位,对应的视图实例不会被销毁
  • 重构位置计算方式:放弃靠HStack、VStack自动堆叠排布位置的逻辑,给每个元素设置统一的固定尺寸,根据元素在当前数组里的下标,计算对应的横向、纵向偏移值,用offset修饰符控制元素的实际展示位置
  • 适配iOS13的动画触发逻辑:iOS13下使用withAnimation包裹数组修改的逻辑块,不需要把动画修饰符挂在最外层容器上,避免全局无意义的动画触发

可直接运行的修正代码

struct ContentView: View {
    @State private var elements = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    // 固定网格列数
    let columnCount = 2
    // 单个网格元素的固定尺寸
    let itemSize: CGFloat = 100
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            // 以元素值为身份遍历所有视图,保证视图实例不会因数组重排被重建
            ForEach(elements, id: \.self) { element in
                Text("element\(element)")
                    .frame(width: itemSize, height: itemSize)
                    .background(Color.gray.opacity(0.2))
                    // 根据元素在数组中的当前下标计算对应的展示位置
                    .offset(
                        x: CGFloat(elements.firstIndex(of: element)! % columnCount) * itemSize,
                        y: CGFloat(elements.firstIndex(of: element)! / columnCount) * itemSize
                    )
            }
            
            Button("Randomize") {
                // iOS13下通过withAnimation包裹状态修改,自动触发位置插值动画
                withAnimation(.easeInOut(duration: 0.3)) {
                    elements.shuffle()
                }
            }
            .padding(.top, itemSize * CGFloat(elements.count / columnCount) + 20)
        }
    }
}

实现效果

元素数组打乱时,每个对应元素的视图实例保持不变,仅根据自身在数组中的新下标更新offset值,SwiftUI会自动计算旧offset到新offset之间的插值过程,实现元素在网格内平滑滑动换位的效果。整套逻辑不依赖iOS14以上的系统组件,可直接在iOS13环境运行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.29 03:54:03