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

SwiftUI:列表中子视图触发动画时列表无联动动画的解决方法

解决SwiftUI List行高与子视图动画不同步的问题

这个问题我之前也碰到过!核心原因是SwiftUI的List底层依赖UIKit的UITableView,子视图内部的@State变化默认不会触发List行布局的动画更新——List根本没收到“行高要变了,得跟着动画”的信号,直接给List加.animation()自然没用。

好在我们可以用PreferenceKey来让子视图把高度变化传递给父视图,同时保持子视图的自包含性(完全符合你想要的MVVM理念,tap手势不用移出去)。下面是具体的修改方案:

步骤1:定义PreferenceKey传递高度信息

首先创建一个PreferenceKey,用来把SubView的高度和唯一ID传递给父视图:

import SwiftUI

// 用来传递子视图的ID和高度
struct ViewHeightKey: PreferenceKey {
    typealias Value = (id: UUID, height: CGFloat)
    static var defaultValue: Value = (UUID(), 0)
    
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = nextValue()
    }
}

步骤2:修改SubView,保持自包含并传递高度

给SubView加一个唯一ID,内部计算目标高度,并用GeometryReader获取当前高度通过Preference传递出去,tap手势完全保留在内部:

struct SubView: View {
    let id: UUID // 每个子视图的唯一标识
    @State private var isExpanded: Bool = false
    
    // 目标高度,根据状态切换
    private var targetHeight: CGFloat { isExpanded ? 300 : 200 }
    
    // 初始化时自动生成唯一ID
    init(id: UUID = UUID()) {
        self.id = id
    }
    
    var body: some View {
        Rectangle()
            .frame(width: 200, height: targetHeight)
            .foregroundColor(.red)
            .onTapGesture {
                withAnimation {
                    isExpanded.toggle()
                }
            }
            .background(
                // 用GeometryReader获取当前视图高度,通过Preference传递给父视图
                GeometryReader { proxy in
                    Color.clear
                        .preference(
                            key: ViewHeightKey.self,
                            value: (id: id, height: proxy.size.height)
                        )
                }
            )
    }
}

步骤3:修改父视图,让List响应高度变化动画

在Test视图中,用@State存储每个行的高度,监听Preference变化,然后给List绑定动画:

struct Test: View {
    @State private var rowHeights: [UUID: CGFloat] = [:]
    
    var body: some View {
        List {
            SubView()
                .onPreferenceChange(ViewHeightKey.self) { data in
                    // 更新对应ID的行高
                    rowHeights[data.id] = data.height
                }
            
            SubView()
                .onPreferenceChange(ViewHeightKey.self) { data in
                    rowHeights[data.id] = data.height
                }
        }
        // 当行高字典变化时,触发List的动画
        .animation(.easeInOut, value: rowHeights)
    }
}

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

为什么这个方案有效?

  • 子视图完全自包含:tap手势、状态管理都在SubView内部,不用依赖父视图控制,完美符合MVVM的组件封装理念。
  • PreferenceKey充当了子视图和父视图的“通信桥梁”,把内部的高度变化传递给List,让List感知到行高需要动画更新。
  • .animation(value: rowHeights)确保只有当行高实际变化时才触发动画,避免不必要的动画干扰。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:38:21