SwiftUI卡片展开动画优化:解决抖动与联动不流畅问题
问题描述
想要实现一个展示不同订阅卡片的视图,切换卡片时展开显示详情,且卡片动画能平滑影响其他卡片,实现无缝移位效果。目前已完成的动画存在抖动问题,尝试使用matchedGeometryEffect但未达到预期效果。
当前效果:
原代码:
import SwiftUI struct ContentView: View { @State private var toogle1: Bool = false @State private var toogle2: Bool = false var body: some View { VStack(spacing: 0) { ScrollView { VStack { subscriptionCurrentYearView() subscriptionPreviousYearView() } .padding(16) } } } @ViewBuilder func subscriptionCurrentYearView() -> some View { SubscriptionView(toggleOn: $toogle1) } @ViewBuilder func subscriptionPreviousYearView() -> some View { SubscriptionView(toggleOn: $toogle2) } } struct SubscriptionView: View { @Namespace private var animation @Binding var toggleOn: Bool var body: some View { VStack { HStack { Text("I want to subscribe this product from the current year") .foregroundColor(.secondary) .frame(width: 250, alignment: .leading) Toggle("", isOn: $toggleOn) .frame(width: 50) } .matchedGeometryEffect(id: "Header", in: animation) .padding() if toggleOn { VStack(alignment: .leading) { Text("Estimated current balance") TextField("", text: .constant("$ 0.00")) .font(.title) Text("Your whole amount must be transferred for the current tax year") } .matchedGeometryEffect(id: "Content", in: animation) .foregroundColor(.secondary) .padding() } } .frame(maxHeight: .infinity) .padding() .background(Color(uiColor: UIColor.lightGray)) .clipShape(RoundedRectangle(cornerRadius: 20)) .animation(.linear, value: toggleOn) } } #Preview { ContentView() }
解决方案
抖动问题核心原因:
- 每个卡片独立使用命名空间,
matchedGeometryEffect无法跨视图协调动画 - 冗余的
padding和frame(maxHeight: .infinity)导致布局计算冲突 - 动画应用层级错误,未让父容器统一控制过渡
修改后的实现代码:
import SwiftUI struct ContentView: View { @State private var toggle1: Bool = false @State private var toggle2: Bool = false // 父视图统一管理命名空间,让所有卡片共享动画上下文 @Namespace private var animationNamespace var body: some View { VStack(spacing: 0) { ScrollView { // 给卡片容器添加统一动画,确保移位平滑 VStack(spacing: 16) { SubscriptionView( title: "I want to subscribe this product from the current year", toggleOn: $toggle1, animationNamespace: animationNamespace ) SubscriptionView( title: "I want to subscribe this product from the previous year", toggleOn: $toggle2, animationNamespace: animationNamespace ) } .padding(16) .animation(.easeInOut(duration: 0.3), value: [toggle1, toggle2]) } } } } struct SubscriptionView: View { let title: String @Binding var toggleOn: Bool // 接收父视图传递的共享命名空间 let animationNamespace: Namespace.ID var body: some View { VStack(spacing: 0) { HStack { Text(title) .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .leading) Toggle("", isOn: $toggleOn) .frame(width: 50) } .padding() if toggleOn { VStack(alignment: .leading, spacing: 8) { Text("Estimated current balance") TextField("", text: .constant("$ 0.00")) .font(.title) Text("Your whole amount must be transferred for the current tax year") .font(.caption) } .padding() // 给每个卡片的展开内容分配唯一ID,避免冲突 .matchedGeometryEffect(id: "Content_\(title)", in: animationNamespace) } } .background(Color(UIColor.lightGray)) .clipShape(RoundedRectangle(cornerRadius: 20)) } } #Preview { ContentView() }
关键修改说明:
- 共享命名空间:将命名空间移到父视图,确保所有卡片的动画上下文统一,
matchedGeometryEffect能正确协调过渡 - 简化布局:移除冗余的
padding和frame(maxHeight: .infinity),消除布局计算抖动 - 统一动画控制:在卡片容器的
VStack上应用动画,使用easeInOut让移位过渡更自然 - 唯一ID区分:给每个卡片的展开内容分配带标题的唯一ID,避免不同卡片的内容动画冲突
内容的提问来源于stack exchange,提问作者miltenkot
相关产品推荐
相关产品推荐

