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

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()
}
解决方案

抖动问题核心原因:

  1. 每个卡片独立使用命名空间,matchedGeometryEffect无法跨视图协调动画
  2. 冗余的padding和frame(maxHeight: .infinity)导致布局计算冲突
  3. 动画应用层级错误,未让父容器统一控制过渡

修改后的实现代码:

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 14:47:24