SwiftUI List 中 cell 高度默认不会随着内容一起平滑插值,而是忽略过渡会「跳变」。需要用 Animatable + Layout + LayoutValueKey + 自己维护一份 displayValue 状态机,来把高度和 spacing 都变成显式、可控的动画状态。全程没有用到 UIKit(摊手)。
SwiftUI List 中 cell 高度默认不会随着内容一起平滑插值,而是忽略过渡会「跳变」。需要用 Animatable + Layout + LayoutValueKey + 自己维护一份 displayValue 状态机,来把高度和 spacing 都变成显式、可控的动画状态。全程没有用到 UIKit(摊手)。
本文将 SwiftUI 定位为声明式动画的核心框架,但其 List 容器在行高动态变化时无法自动提供高度插值,导致跳变问题。作者通过纯原生能力(Animatable、自定义 Layout 等)解决这一限制,避免了回退到 UIKit 或放弃动画。文章强调 SwiftUI 的布局信息单向流动特性是问题根源,同时指出其能力边界比初次遇到的限制更宽。
本文指出 List 因绑定宿主平台滚动机制,为保证复用与性能,会在内容高度变化时直接硬性重排,无法对 row 高度进行连续动画插值。典型场景包括副标题显隐或文本行数变化,导致高度跳变而非平滑过渡。解决方案围绕 List 内部构建自定义布局容器,实现高度与间距的显式动画控制。
本文核心方案是实现 VisibleHeightLayout 与 CollapsibleSpacingVStack 两个自定义 Layout,用于分离父容器申报尺寸与子视图实际摆放尺寸。sizeThatFits 返回动画中的可见高度,placeSubviews 则始终使用子视图 intrinsic 尺寸,避免内容被压缩。配合 clipped() 实现裁剪而非挤压的折叠效果,同时为目标高度测量提供稳定锚点。
本文将 Animatable 协议作为 SwiftUI 提供的动画插值工具之一,与 LayoutValueKey 结合使用。通过让 ViewModifier 遵循该协议,可在事务内逐帧重写 layoutValue,使原本静态的布局属性获得动画能力。作者用此机制让 CollapsibleSpacingVStack 中的间距随折叠进度同步变化,避免二段跳变。
本文将 geometryGroup 列为 SwiftUI 提供的两个高度动画工具之一(另一个是 Animatable),用于将动画插值过程显式化或上提。但实际解决方案主要依赖自定义 Layout 协议,未深入使用 geometryGroup 作为核心手段。
本文在 CollapsibleSpacingVStack 中使用 LayoutValueKey 让子视图显式声明折叠状态与进度(0...1)。通过 Animatable ViewModifier 间接实现该值的动画插值,父布局据此按比例缩减相邻间距,并在折叠完成后为新相邻 sibling 补回 bridge spacing。解决了 spacing 突变导致的视觉不连贯问题。
本文将 UIKit 作为命令式框架的代表,指出其可通过预计算尺寸与 performBatchUpdates 实现先算后交的动画控制。SwiftUI 的响应式数据流则缺少这一缓冲区,导致 List 无法自动为动态行高提供插值。文章最终给出的方案完全不依赖 UIKit,而是通过原生 Layout 协议与状态解耦达成类似效果。
文中对比了普通 VStack 与 List 的差异:VStack 或 LazyVStack 中内容显隐通常可正常动画,但放入 List 后行高变为硬切。标准 VStack 对子视图的 spacing 承诺是无条件的,即使子视图高度折叠至 0 仍会保留间距,导致动画末段出现二段跳。文章为此设计了 CollapsibleSpacingVStack 作为替代。
文章指出,把带 if 显隐与 animation 的代码放在 VStack 或 LazyVStack 中,行高变化通常能平滑过渡。但相同代码放入 List 后,高度却出现硬切换与闪烁。LazyVStack 因此被用来衬托 List 在复用与尺寸计算上的特殊约束,以及自定义 Layout 方案的必要性。
文中利用 ViewModifier 遵循 Animatable 的特性,让原本静态的 LayoutValueKey 获得逐帧插值能力。通过在 body 中重写 layoutValue,CollapsibleSpacingVStack 能够读取 AnimatedPresence 暴露的折叠进度,从而把 spacing 变化同步到同一动画时间线。
本文核心组件之一,用于封装内容显隐并完成高度测量与动画。内部通过 displayValue 状态机解耦数据与渲染生命周期,配合 VisibleHeightLayout 向父容器报告可见高度、向子视图保持 intrinsic 尺寸,避免内容被压缩。最终实现 List row 高度的连续插值过渡。
为解决折叠时 spacing 突变而设计的 VStack 复刻版。它识别带有 ignoredWhenCollapsed 标记的子视图,按 collapsibleSpacingProgress 动态缩放相邻间距,并在折叠区段前后补 bridge spacing。配合 AnimatedPresence,使高度与间距变化锁定在同一动画曲线上,消除 List 中的二段跳。
VisibleHeightLayout 是本文提出的自定义 Layout 容器,专门用于解决 List 行高动画跳变。它在 sizeThatFits 中向父容器返回 (intrinsic.width, visibleHeight),而在 placeSubviews 时始终用子视图的 intrinsic 尺寸进行放置,从而实现父级高度连续插值而子内容不被压缩。配合 .clipped(),该布局既保证动画平滑,又为 AnimatedPresence 提供了稳定的目标高度测量锚点,是整套原生方案的核心组件。
可左右滑动查看