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

SwiftUI:如何在List内实现内容尺寸变化的平滑动画?

这个问题确实是SwiftUI List的一个常见痛点——系统默认的行高计算是即时生效的,不会跟随内部视图的动画节奏走。不过有几种方案可以解决这个问题,让行高和内容的动画完美同步:

方案一:用UIKit的UITableView包装(推荐,保留List原生交互)

因为UIKit的UITableView对行高动画的支持更成熟,我们可以用UIViewRepresentable把它包装成SwiftUI可用的组件,同时让行高变化和内容视图的动画绑定:

import SwiftUI
import UIKit

// 包装支持动画行高的UITableView
struct AnimatedList<Content: View>: UIViewRepresentable {
    let content: () -> Content
    @Binding var rowHeights: [CGFloat]
    
    func makeUIView(context: Context) -> UITableView {
        let tableView = UITableView()
        tableView.delegate = context.coordinator
        tableView.dataSource = context.coordinator
        tableView.register(AnimatedListCell.self, forCellReuseIdentifier: "Cell")
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 200 // 设置初始预估高度
        return tableView
    }
    
    func updateUIView(_ uiView: UITableView, context: Context) {
        uiView.reloadData()
        // 触发行高同步动画
        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {
            uiView.beginUpdates()
            uiView.endUpdates()
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource {
        let parent: AnimatedList
        
        init(parent: AnimatedList) {
            self.parent = parent
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            parent.rowHeights.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! AnimatedListCell
            cell.hostingController.rootView = parent.content()
            cell.hostingController.view.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: parent.rowHeights[indexPath.row])
            return cell
        }
        
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            parent.rowHeights[indexPath.row]
        }
    }
}

// 承载SwiftUI视图的UITableViewCell
class AnimatedListCell: UITableViewCell {
    let hostingController = UIHostingController(rootView: EmptyView())
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        addSubview(hostingController.view)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// 调整后的动态矩形视图
struct DynamicRectangle: View {
    @Binding var height: CGFloat
    let initialHeight: CGFloat = 200
    let expandedHeight: CGFloat = 400
    
    var body: some View {
        Rectangle()
            .fill(Color.blue)
            .frame(height: height)
            .onTapGesture {
                withAnimation(.spring()) {
                    height = height == initialHeight ? expandedHeight : initialHeight
                }
            }
    }
}

// 使用示例
struct ContentView: View {
    @State private var rowHeights = [200, 200, 200]
    
    var body: some View {
        AnimatedList(rowHeights: $rowHeights) {
            ForEach(0..<3) { index in
                DynamicRectangle(height: $rowHeights[index])
            }
        }
    }
}

这个方案的核心逻辑:

  • 用UIHostingController把SwiftUI视图嵌入到UITableViewCell中
  • 通过绑定的rowHeights数组控制每行高度,点击触发动画时,行高变化会通过UIView动画块和内容视图动画同步
  • 保留了UITableView的原生交互(如滑动删除、下拉刷新等,可按需扩展)

方案二:用ScrollView模拟List(简单,适合轻量场景)

如果不需要List的复杂交互,直接用ScrollView加VStack模拟List,内容视图的动画会自然带动行高变化,完全同步:

import SwiftUI

struct DynamicRectangle: View {
    @State private var isExpanded = false
    var body: some View {
        Rectangle()
            .fill(Color.blue)
            .frame(height: isExpanded ? 400 : 200)
            .animation(.spring(), value: isExpanded)
            .onTapGesture {
                isExpanded.toggle()
            }
            .padding(.horizontal)
            .padding(.vertical, 8)
    }
}

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 0) {
                DynamicRectangle()
                Divider() // 模拟List分割线
                DynamicRectangle()
                Divider()
                DynamicRectangle()
            }
        }
    }
}

这个方案优点是简单直接,无需和UIKit交互;缺点是丢失了List的原生功能(如滑动操作、行选中效果等)。

为什么原代码会出现不同步问题?

SwiftUI的List行高由系统自动计算,且这个计算是即时生效的,不会等待内部视图的动画完成。所以当你点击矩形触发高度动画时,List已经立刻把行高调整到目标值,而内部视图还在过渡,就出现了行高跳变、动画不同步的现象。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:43:46