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

SwiftUI List中自定义DisclosureGroup展开动画失效如何修复?

修复List中自定义DisclosureGroup的展开动画失效问题

问题根源

在List中使用自定义DisclosureGroup时,动画效果异常的核心原因有三点:

  1. List的单元格存在复用机制,组件内部的@State状态会因复用被意外重置或共享,导致动画逻辑混乱
  2. 直接通过条件渲染显示/隐藏内容,SwiftUI无法捕捉到完整的布局变化,无法生成平滑的过渡动画
  3. 系统默认DisclosureGroup与List的布局系统深度整合,自定义组件缺少对应的适配逻辑

修复方案

以下是调整后的完整代码,解决动画失效问题并对齐系统默认组件的交互体验:

1. 重构自定义DisclosureGroup组件

将状态管理外置,添加过渡动画与视觉适配:

struct CustomDisclosureGroup<Label: View, Content: View>: View {
    @Binding var isExpanded: Bool
    private let label: Label
    private let content: Content

    init(isExpanded: Binding<Bool>, @ViewBuilder label: () -> Label, @ViewBuilder content: () -> Content) {
        self._isExpanded = isExpanded
        self.label = label()
        self.content = content()
    }

    var body: some View {
        VStack(spacing: 0) {
            Button {
                withAnimation(.default) {
                    isExpanded.toggle()
                }
            } label: {
                HStack {
                    label
                    Spacer()
                    // 模拟系统箭头的旋转动画
                    Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
                        .rotationEffect(.degrees(isExpanded ? 0 : -90))
                        .animation(.default, value: isExpanded)
                }
                .contentShape(Rectangle()) // 让整个按钮区域可点击
            }

            // 添加过渡动画,确保平滑展开/收起
            content
                .frame(maxWidth: .infinity, alignment: .leading)
                .transition(.disclosureTransition()) // iOS16+ 系统原生过渡效果
                .opacity(isExpanded ? 1 : 0)
                .scaleEffect(y: isExpanded ? 1 : 0.95, anchor: .top)
        }
        .listRowInsets(EdgeInsets()) // 去掉默认行内边距,对齐系统样式
    }
}

2. 父视图管理展开状态

使用字典存储每个列表项的展开状态,避免List复用导致的状态混乱:

struct ContentView: View {
    @State var items: [Int] = [0, 1, 2, 3]
    @State private var expandedStates: [Int: Bool] = [:]

    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                CustomDisclosureGroup(
                    isExpanded: Binding(
                        get: { expandedStates[item, default: false] },
                        set: { expandedStates[item] = $0 }
                    ),
                    label: {
                        Text(item.formatted())
                    }, content: {
                        Text("Test text")
                        Text("Test text")
                        Text("Test text")
                    }
                )
            }
        }
    }
}

关键修改点说明

  • 状态外置:将展开状态从组件内部@State改为外部绑定,由父视图通过字典独立管理每个列表项的状态,彻底避免List复用带来的状态问题
  • 过渡动画:使用.disclosureTransition()(iOS16+)或手动组合opacity、scaleEffect过渡,让内容的显示/隐藏有自然的动画效果
  • 视觉适配:添加箭头图标并实现旋转动画,模拟系统DisclosureGroup的交互反馈;设置.listRowInsets(EdgeInsets())去掉默认边距,对齐系统样式
  • 动画一致性:明确指定.default动画类型,确保点击触发的动画逻辑稳定

内容的提问来源于stack exchange,提问作者Nicolas Gimelli

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 10:46:30