添加子视图时在 SwiftUI 列表行中停止视图 "bouncing"?

Stop views "bouncing" in a SwiftUI List row when adding child views?

我正在构建一个应用程序,其中与一系列参数相关的摘要信息和历史详细信息显示在列表中。

用户可以通过单击披露按钮来查看历史指标,DisclosureGroup 处理得非常巧妙,但我想添加一个按钮来切换控件的显示,使用户无需使用模式显示即可输入新数据或离开摘要屏幕。

尽管您可以嵌套 DisclosureGroups,但这在 IMO 上不是很好的 UX(用户可能需要公开两次才能访问他们想要的信息)所以我想通过有条件地向层次结构添加视图来复制 DisclosureGroup 动画行为。

在列表或表单之外,这很容易做到 - 确保切换显示标志的操作包含在 withAnimation 中,SwiftUI 会处理其余部分。不幸的是,这些容器视图的布局方式会导致一些相当奇怪的行为,导致在将新视图添加到单元格时,预先存在的视图(文本视图除外)“弹跳”。 DisclosureGroup 不会发生这种情况(无论您使用的是文本还是其他视图子类型),它在添加到列表或表单时完全不受影响。

在模拟器中放慢动画速度可以明显看出父视图(即单元格)在添加新子视图之前更改了其大小,但这会导致现有视图的位置也发生更改(试图通过在封闭的 HStack 上或直接在视图的框架上使用对齐修饰符将其锚定到单元格视图的顶部不会阻止这种情况发生。

此外,当视图从单元格的层次结构中删除时,单元格会在视图被删除之前缩小,离开的视图会在相邻的单元格上悬停一小段时间,看起来很乱。不用说,DisclosureGroup 不会发生这种情况,所以我认为我可以合理地假设我所追求的是完全可能的。

我已经尝试(但失败了)通过向每个视图抛出对齐、HStack 和 VStack 容器以及 .matchedGeometryEffect 修饰符来解决问题(后者似乎是最有希望的,因为这似乎最终协调不佳视图转换)。这要么不起作用,要么导致了一些非常奇怪的行为。

放慢动画速度可以明显看出 DisclosureGroup 可能正在使用 ZStack and/or 自定义过渡,它以渐进方式偏移子视图。然而,我尝试复制它一直......有趣。

一张图片值一千字(所以一个视频值一万字?)请参阅附件中的行为说明以及相关代码。

使用文本视图:

struct DisclosureWithTextView: View {
@Namespace private var nspace

@State private var willDisplayControl = false

var body: some View {
        DisclosureGroup(
            content: {
                ForEach(0..<2) { index in
                    Text("Item")
                        .padding(6)
                        .background {
                            Rectangle()
                                .foregroundColor(.yellow)
                        }
                }
            },
            label: {
                VStack {
                    HStack {
                        Button(action: {
                            willDisplayControl.toggle()
                        })
                        {
                            Image(systemName: "plus.circle")
                        }
                        .buttonStyle(PlainButtonStyle())

                        Text("SUMMARY")
                            .padding(8)
                            .background {
                                Rectangle()
                                    .foregroundColor(.red)
                            }
                    }

                    if willDisplayControl {
                        Text("CONTROL")
                            .padding(8)
                            .background {
                                Rectangle()
                                    .foregroundColor(.green)
                            }
                    }
                }

            })

}

}

使用矩形视图:

struct DisclosureWithGraphics: View {
@Namespace private var nspace

@State private var willDisplayControl = false

var body: some View {
    DisclosureGroup(
        content: {
            ForEach(0..<2) { index in
                Rectangle()
                    .frame(width:80, height: 30)
                    .foregroundColor(.yellow)
            }
        },
        label: {
            VStack {
                HStack {
                    Button(action: {
                        withAnimation { willDisplayControl.toggle() }
                    })
                    {
                        Image(systemName: "plus.circle")
                    }
                    .buttonStyle(PlainButtonStyle())

                    Rectangle()
                        .frame(width: 110, height: 40)
                        .foregroundColor(.red)

                }

                if willDisplayControl {
                    Rectangle()
                        .frame(width: 100, height: 40)
                        .foregroundColor(.green)
                }
            }

        })
}

}

具有讽刺意味的是,我意识到 Asperi 向我介绍了他的解决方案,以解决我 18 个月前提出的几乎相同的问题!现在感觉有点傻,但这解释了我以前遇到过这个问题的模糊感觉!

但是,我发现有一个重要的区别。如果您在 List 或 Form 中使用 'vanilla' 堆栈容器视图,则只需调整对齐方式即可实现一个视图被另一个视图显示的流畅动画效果。对容器视图的框架进行动画处理绝对没问题,但这不是必需的。但是,一旦您在 DisclosureGroup 中尝试相同的操作,无论您是否为对齐指南、框架等设置动画,构成标签的视图都会开始跳跃,并且您必须明确地为父视图高度设置动画。

因此,在简单的 List 或 Form 单元格中滑出视图很容易(使用 AlignmentGuide),但是当您在更专业的视图容器中尝试相同的操作时,您需要为单元格高度添加 AnimatableValue。如果我猜的话,我怀疑这是因为与 Apple 自己的披露实施存在一些冲突。

所以,感谢 Asperi 提醒我该怎么做:-)

下面两种实现效果的代码示例(我个人比较喜欢对齐引导动画的样子)

正在调整要公开的视图的框架:

struct DisclosureWithFrameAnimationReveal: View {
@Namespace private var nspace
@State private var willDisplayControl = false

var body: some View {

        DisclosureGroup(
            content: {
                ForEach(0..<2) { index in
                    Text("Item")
                        .padding(6)
                        .background {
                            Rectangle()
                                .foregroundColor(.yellow)
                        }
                }
            },
            label: {
                VStack {
                    HStack {
                        Button(action: {
                            withAnimation(.easeInOut) { willDisplayControl.toggle() }
                        })
                        {
                            Image(systemName: "plus.circle")
                        }
                        .buttonStyle(PlainButtonStyle())

                        Color(.red)
                            .frame(width: 100, height: 40)
                        Spacer()
                    }
                    .padding(.top, 4)
                    .background()

                    HStack {
                        Color(.blue)
                            .frame(width: 100, height: willDisplayControl ? 40 : 0)
                        Spacer()
                    }
                    .opacity(willDisplayControl ? 1 : 0)
                }
                .modifier(AnimatableCellHeight(height: willDisplayControl ? 88 : 44))

            }
        )
}
}

对齐引导动画:

struct DisclosureWithAlignmentReveal: View {
@Namespace private var nspace
@State private var willDisplayControl = false

var body: some View {
        DisclosureGroup(
            content: {
                ForEach(0..<2) { index in
                    Text("Item")
                        .padding(6)
                        .background {
                            Rectangle()
                                .foregroundColor(.yellow)
                        }
                }
            },
            label: {
                ZStack(alignment: .top) {
                    HStack {
                        Button(action: {
                            withAnimation(.easeInOut) { willDisplayControl.toggle() }
                        })
                        {
                            Image(systemName: "plus.circle")
                        }
                        .buttonStyle(PlainButtonStyle())

                        Color(.red)
                            .frame(width: 100, height: 40)
                        Spacer()
                    }
                    .zIndex(1)
                    .padding(.top, 4)
                    .background()

                    HStack {
                        Color(.blue)
                            .frame(width: 100, height: 40)
                        Spacer()
                    }
                    .alignmentGuide(.top, computeValue: { d in d[.top] - (willDisplayControl ? 46 : 0) })
                    .opacity(willDisplayControl ? 1 : 0)
                    .zIndex(0)
                }
                .modifier(AnimatableCellHeight(height: willDisplayControl ? 88 : 44))
            }
        )
}
}

最后,AnimatableValue 实现:

struct AnimatableCellHeight: AnimatableModifier {
var height: CGFloat = 0

var animatableData: CGFloat {
    get { height }
    set { height = newValue }
}

func body(content: Content) -> some View {
    content.frame(height: height)
}
}