【问题标题】:ScrollView + NavigationView animation glitch SwiftUIScrollView + NavigationView 动画故障 SwiftUI
【发布时间】:2021-01-24 13:24:48
【问题描述】:

我有一个简单的看法:

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

【问题讨论】:

    标签: swift animation swiftui scrollview


    【解决方案1】:

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

    1. 滚动视图不会在 NavigationView 和 TabView 下扩展 - 这使得它失去了滚动条下内容的美丽模糊效果。
    2. 在滚动视图上设置背景将导致大标题 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()
            }
        }
    }
    

    【讨论】:

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

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-31
      • 1970-01-01
      • 2022-09-27
      • 2014-11-24
      • 2013-07-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多