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
相关产品推荐
相关产品推荐

