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<Content: View> ...
的东西,它只是在正文中创建一个 ScrollView
但内容类型是推断出来的,所以我们不需要这种类型转换。【参考方案4】:
在面对同样的问题时,我做了一些调查。 故障似乎来自滚动视图弹跳和滚动上下文减速速度的组合。
现在我已经设法通过将减速率设置为快来消除故障。它似乎让 swiftui 更好地计算布局,同时保持弹跳动画处于活动状态。
我的解决方法很简单,只需在视图的 init 中设置以下内容。缺点是这会影响滚动减速的速度。
init()
UIScrollView.appearance().decelerationRate = .fast
一个可能的改进是计算正在显示的内容的大小,然后根据需要动态切换减速率。
【讨论】:
以上是关于ScrollView + NavigationView 动画故障 SwiftUI的主要内容,如果未能解决你的问题,请参考以下文章
属性 'scrollView' 需要定义方法 '-scrollView' - 使用 @synthesize、@dynamic 还是提供方法实现?