提问者:小点点

添加子视图时停止SwiftUI列表行中的视图“跳动”?


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

用户可以通过单击disclosure按钮来查看历史指标,DisclosureGroup非常灵活地处理了这一点,但我想添加一个按钮来切换控件的显示,使用户能够输入新数据,而无需使用模式显示或离开摘要屏幕。

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

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

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

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

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

放慢动画速度可以明显看出 DisclosureGroup 可能正在使用 ZStack 和/或自定义过渡,它们以渐进的方式抵消子视图。但是,我试图复制这一点的尝试是...有趣。

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

使用文本视图:

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)
                }
            }

        })
}

}


共1个答案

匿名用户

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

然而,我发现有一个重要的区别。如果你在一个列表或表单中使用一个“普通的”堆栈容器视图,你只需要调整对齐就可以实现一个视图被另一个视图显示的平滑动画。动画容器视图的框架工作得非常好,但这不是必需的。但是,一旦您在DisclosureGroup中尝试同样的操作,无论您是否激活对齐参考线、框架等,构成您标签的视图都会开始跳跃,并且您必须显式激活父视图高度。

因此,在一个简单的列表或表单单元格中滑出视图很容易(使用对齐指南),但是当你在一个更专业的视图容器中尝试同样的事情时,你需要添加动画值作为单元格高度。如果我猜,我怀疑这是因为与苹果自己的披露实现有一些冲突。

所以,感谢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))
            }
        )
}
}

以及,最后的,动画值实现:

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)
}
}