为 SwiftUI 组件的显示与隐藏添加动画
了解如何使用 .transition() 修饰器为组件的显隐添加动画,并理解为什么使用 if 控制组件的显隐时,在某些场景下无法正常工作?
在之前的文章中,我们已经详细介绍过 SwiftUI 中的 .transition()
、.animation()
修饰器,以及 withAnimation()
函数的作用与使用场景:
这篇文章我们重点介绍,使用 .transition()
为组件的显隐添加动画时,一些常见错误以及最佳实践方案。
以下代码是一个常见的场景:
- 当满足特定条件时,会显示
ReimbursementProgressView
视图(报销状态) - 不满足条件,则不显示报销状态组件
// MARK: 报销进度
if isReimbursementEnabled
&& record.reimbursementStatus != .none
{
ReimbursementProgressView(record: record)
}
默认情况下,组件的显示和隐藏会直接发生,没有过渡动画:
避免将 .animation() 添加到 If 语句内部
可以使用 .transition()
搭配 .animation()
为组件的显隐添加动画:
// MARK: 报销进度
if isReimbursementEnabled
&& record.reimbursementStatus != .none
{
ReimbursementProgressView(record: record)
.animation(
.default, value: record.reimbursementStatus
)
.transition(.opacity)
}
但实际运行,你会发现代码并未按照预期工作。原因是 .animation()
修饰器在视图层级中的应用位置不正确。
.animation()
修饰器必须应用到包含条件逻辑的父视图上,它告诉 SwiftUI 为父视图中的视图层级变化设置动画。也就是说,.animation() 必须放到 if 语句外部。
使用 Stack 包裹条件内容
一种简单的解决方案是,用 Stack 或其他容器包裹条件内容,然后将 .animation 修饰器应用到父视图。
例如,使用 VStack 包裹:
VStack(spacing: 0) {
if isReimbursementEnabled
&& record.reimbursementStatus != .none
{
ReimbursementProgressView(record: record)
.transition(.opacity)
}
}.animation(
.spring(duration: 1), value: record.reimbursementStatus
)
这样修改之后,动画就可以正常生效了:
Stack 组件占用格外空间问题
使用 Stack 组件时,即使某些子组件未显示,也可能导致布局中占用额外的空间。这通常是因为 Stack 会为其子组件之间的间距(spacing)预留空间,即使这些子组件没有内容:
一种常见场景是 Stack 嵌套,例如:
VStack(spacing:16){
VStack(spacing: 0) {
if isReimbursementEnabled
&& record.reimbursementStatus != .none
{
ReimbursementProgressView(record: record)
.transition(.opacity)
}
}.animation(.spring(duration: 1), value: record.reimbursementStatus)
}
在这个例子中,即使内部的 VStack 没有显示任何内容,由于外部 VStack 设置了 spacing: 16,父布局仍会为组件之间的间距预留空间。
要解决这个问题,需要避免无意义的嵌套。如果内部 VStack 没有特殊作用,可以直接将 if 条件内的内容移到外层 VStack,减少多余的布局层级。
这样优化之后,就不会有占据空间的问题:
VStack(spacing:16){
if isReimbursementEnabled
&& record.reimbursementStatus != .none
{
ReimbursementProgressView(record: record)
.transition(.opacity)
}
}
.animation(.spring(duration: 1), value: record.reimbursementStatus)
最佳实践
将 .animation() 放在视图的最外层
.transition() 和 .animation() 的使用位置会直接影响动画的表现和效果,建议遵循以下原则:
- .transition() 应放在具体的组件上。这样可以明确地为某个组件的显示和隐藏定义过渡效果。
- .animation() 应放在页面的最外层。将 .animation() 修饰器应用到整个页面的最外层,可以确保所有同层级组件的动画同步进行,从而避免不必要的 UI 冲突。
如果在页面的某个子组件中使用 .animation(),会为该组件的显示或隐藏添加动画效果,但同层级的其他组件却不会同步动画。这样可能会导致视觉上的不一致。例如:
VStack(spacing:16){
// 其他组件
VStack(spacing: 0) {
if isReimbursementEnabled
&& record.reimbursementStatus != .none
{
ReimbursementProgressView(record: record)
.transition(.opacity)
}
}.animation(.spring(duration: 1), value: record.reimbursementStatus)
// 其他组件
}
在上述代码中,虽然 ReimbursementProgressView 的显示和隐藏具有动画效果,但当该组件消失时,其它同层级组件的变化不会同步动画,可能导致突兀的 UI 显示效果。
如果将 .animation() 添加到该页面的最外层,当一个组件显示或消失时,其他组件也会自动应用动画。在视觉效果上会更流畅:
VStack(spacing:16){
// 其他组件
VStack(spacing: 0) {
if isReimbursementEnabled
&& record.reimbursementStatus != .none
{
ReimbursementProgressView(record: record)
.transition(.opacity)
}
}
// 其他组件
}
.animation(.spring(duration: 1), value: record.reimbursementStatus)