ScrollView + NavigationView 动画故障 SwiftUI

Posted

技术标签:

【中文标题】ScrollView + NavigationView 动画故障 SwiftUI【英文标题】:ScrollView + NavigationView animation glitch SwiftUI 【发布时间】:2020-10-09 12:49:41 【问题描述】:

我有一个简单的看法:

var body: some View 
        NavigationView 
            ScrollView 
                VStack 
                    ForEach(0..<2)  _ in
                        CardVew(for: cardData)
                    
                
            
            .navigationBarTitle("Testing", displayMode: .automatic)
        
    

但是您可以用任何东西替换 CardView - 故障仍然存在。 Glitch video

有办法解决吗?

Xcode 12.0.1、Swift 5

【问题讨论】:

【参考方案1】:

将顶部填充设置为 1 会破坏至少 2 个主要内容:

    滚动视图未在 NavigationView 和 TabView 下扩展 - 这使其失去了滚动条下内容的美丽模糊效果。 在滚动视图上设置背景将导致大标题 NavigationView 停止折叠。

当我不得不更改我正在开发的应用程序的所有屏幕上的背景颜色时,我遇到了这些问题。 所以我做了更多的挖掘和试验,并设法找到了一个很好的解决方案。

这是原始解决方案:

我们将 ScrollView 包装到 2 个几何阅读器中。

顶部是尊重安全区域 - 我们需要这个来读取安全区域插图 第二个是全屏。

我们将滚动视图放入第二个几何阅读器 - 使其大小为全屏。

然后我们使用 VStack 添加内容,通过应用安全区域填充。

最后 - 我们的滚动视图不会闪烁并接受背景而不破坏导航栏的大标题。

struct ContentView: View 
    var body: some View 
        NavigationView 
            GeometryReader  geometryWithSafeArea in
                GeometryReader  geometry in
                    ScrollView 
                        VStack 

                            Color.red.frame(width: 100, height: 100, alignment: .center)

                            ForEach(0..<5)  i in

                                Text("\(i)")
                                    .frame(maxWidth: .infinity)
                                    .background(Color.green)

                                Spacer()
                            

                            Color.red.frame(width: 100, height: 100, alignment: .center)
                        
                        .padding(.top, geometryWithSafeArea.safeAreaInsets.top)
                        .padding(.bottom, geometryWithSafeArea.safeAreaInsets.bottom)
                        .padding(.leading, geometryWithSafeArea.safeAreaInsets.leading)
                        .padding(.trailing, geometryWithSafeArea.safeAreaInsets.trailing)
                    
                    .background(Color.yellow)
                
                .edgesIgnoringSafeArea(.all)
            
            .navigationBarTitle(Text("Example"))
        
    

优雅的解决方案

既然解决方案现在很明确 - 让我们创建一个优雅的解决方案,只需替换填充修复即可重用并应用于任何现有的 ScrollView。

我们创建一个声明 fixFlickering 函数的 ScrollView 扩展。

逻辑基本上是我们将接收器包装到几何阅读器中,并将其内容包装到带有安全区域填充的 VStack 中 - 就是这样。

使用了 ScrollView,因为编译器错误地推断嵌套滚动视图的内容应该与接收者相同。显式声明 AnyView 将使其接受包装的内容。

有 2 个重载:

第一个不接受任何参数,您可以在任何现有的滚动视图上调用它,例如。您可以将 .padding(.top, 1) 替换为 .fixFlickering() - 就是这样。 第二个接受配置器闭包,用于让您有机会设置嵌套滚动视图。这是必需的,因为我们不使用接收器而只是包装它,但我们创建了一个新的 ScrollView 实例并仅使用接收器的配置和内容。在这个闭包中,您可以以任何您想要的方式修改提供的 ScrollView,例如。设置背景颜色。
extension ScrollView 
    
    public func fixFlickering() -> some View 
        
        return self.fixFlickering  (scrollView) in
            
            return scrollView
        
    
    
    public func fixFlickering<T: View>(@ViewBuilder configurator: @escaping (ScrollView<AnyView>) -> T) -> some View 
        
        GeometryReader  geometryWithSafeArea in
            GeometryReader  geometry in
                configurator(
                ScrollView<AnyView>(self.axes, showsIndicators: self.showsIndicators) 
                    AnyView(
                    VStack 
                        self.content
                    
                    .padding(.top, geometryWithSafeArea.safeAreaInsets.top)
                    .padding(.bottom, geometryWithSafeArea.safeAreaInsets.bottom)
                    .padding(.leading, geometryWithSafeArea.safeAreaInsets.leading)
                    .padding(.trailing, geometryWithSafeArea.safeAreaInsets.trailing)
                    )
                
                )
            
            .edgesIgnoringSafeArea(.all)
        
    

示例 1

struct ContentView: View 
    var body: some View 
        NavigationView 
            ScrollView 
                VStack 
                    Color.red.frame(width: 100, height: 100, alignment: .center)

                    ForEach(0..<5)  i in

                        Text("\(i)")
                            .frame(maxWidth: .infinity)
                            .background(Color.green)
                        
                        Spacer()
                    

                    Color.red.frame(width: 100, height: 100, alignment: .center)
                
            
            .fixFlickering  scrollView in
                
                scrollView
                    .background(Color.yellow)
            
            .navigationBarTitle(Text("Example"))
        
    

示例 2

struct ContentView: View 
    var body: some View 
        NavigationView 
            ScrollView 
                VStack 
                    Color.red.frame(width: 100, height: 100, alignment: .center)

                    ForEach(0..<5)  i in

                        Text("\(i)")
                            .frame(maxWidth: .infinity)
                            .background(Color.green)
                        
                        Spacer()
                    

                    Color.red.frame(width: 100, height: 100, alignment: .center)
                
            
            .fixFlickering()
            .navigationBarTitle(Text("Example"))
        
    

【讨论】:

谢谢你这么详细的解释,非常感谢 这几乎是完美的,只是它没有更新scrollIndicatorInsets!遗憾的是,我想知道您是否需要 UIKit 才能真正做到这一点。 不幸的是,它不适用于 Stack V Lazy 的 pinnedView 很好的解释,但由于AnyView而不会实现它 是否可以更新 ScrollView Indicators,因为该指标正在向上扩展,看起来不对【参考方案2】:

这里有一个解决方法。将.padding(.top, 1) 添加到ScrollView

struct ContentView: View 
            
    var body: some View 
        NavigationView 
            ScrollView 
                VStack 
                    ForEach(0..<2)  _ in
                        Color.blue.frame(width: 350, height: 200)
                    
                
            
            .padding(.top, 1)
            .navigationBarTitle("Testing", displayMode: .automatic)
        
            
    

【讨论】:

感谢您发布此信息!我很好奇是什么思维过程导致你找到那个解决方案...... @PeterFriese,我开始在scrollView 上方放置一个额外的视图,认为scrollView 和navigationBar 之间存在交互。那行得通。然后我减小了该视图的大小,发现它可能非常短并且仍然有效。由于这个视图充当填充,我决定尝试用填充替换它,并看到它也有效。所以公式是一部分直觉和一部分尝试的东西。 @PeterFriese,如果您查看编辑历史记录,您会看到我的第一个解决方案带有额外的视图。 在 swiftui 使用了 2 年之后,我们仍然需要这些变通方法,这太疯狂了!无论如何,谢谢! 顺便说一句,这会破坏导航视图下方的滚动视图扩展内容【参考方案3】:

我简化了@KoCMoHaBTa's answer。这里是:

extension ScrollView 
    private typealias PaddedContent = ModifiedContent<Content, _PaddingLayout>
    
    func fixFlickering() -> some View 
        GeometryReader  geo in
            ScrollView<PaddedContent>(axes, showsIndicators: showsIndicators) 
                content.padding(geo.safeAreaInsets) as! PaddedContent
            
            .edgesIgnoringSafeArea(.all)
        
    

这样使用:

struct ContentView: View 
    var body: some View 
        NavigationView 
            ScrollView 
                /* ... */
            
            .fixFlickering()
        
    

【讨论】:

关于_PaddingLayout的TIL - 这在任何地方都有记录吗?这看起来更优雅但也更脆弱。 @steipete 我通常通过打印 SwiftUI 视图并读取它产生的类型来找到类似的东西。我还没有发现它记录在任何地方。如果您有兴趣,我还发现了 Xcode 自动完成初始化器,例如 AnyView(_fromValue:),我认为它是私有的。 @steipete 虽然这个解决方案是“强大的”,但我不确定使用诸如 _PaddingLayout 之类的东西是否算作“私有 API”。毕竟,通常会推断出类型。如果您像我一样有点不确定,您可以制作类似 struct ContentAcceptingScrollView&lt;Content: View&gt; ... 的东西,它只是在正文中创建一个 ScrollView 但内容类型是推断出来的,所以我们不需要这种类型转换。【参考方案4】:

在面对同样的问题时,我做了一些调查。 故障似乎来自滚动视图弹跳和滚动上下文减速速度的组合。

现在我已经设法通过将减速率设置为快来消除故障。它似乎让 swiftui 更好地计算布局,同时保持弹跳动画处于活动状态。

我的解决方法很简单,只需在视图的 init 中设置以下内容。缺点是这会影响滚动减速的速度。

 init() 
    UIScrollView.appearance().decelerationRate = .fast

一个可能的改进是计算正在显示的内容的大小,然后根据需要动态切换减速率。

【讨论】:

以上是关于ScrollView + NavigationView 动画故障 SwiftUI的主要内容,如果未能解决你的问题,请参考以下文章

排列多个scrollView

android 怎么让scrollview不能滑动

属性 'scrollView' 需要定义方法 '-scrollView' - 使用 @synthesize、@dynamic 还是提供方法实现?

Scrollview总结:滑动问题监听Scrollview实现头部局改变

控件 -- ScrollView

cocos scrollview和pageview 的区别