ScrollView + NavigationView 动画故障 SwiftUI
ScrollView + NavigationView animation glitch SwiftUI
我有一个简单的看法:
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(0..<2) { _ in
CardVew(for: cardData)
}
}
}
.navigationBarTitle("Testing", displayMode: .automatic)
}
}
但是您可以用任何东西替换 CardView - 故障仍然存在。 Glitch video
有办法解决吗?
Xcode12.0.1,Swift5
这是一个解决方法。将 .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)
}
}
}
将顶部填充设置为 1 至少破坏了 2 个主要方面:
- 滚动视图不会在 NavigationView 和 TabView 下扩展 - 这使得它失去了在栏下滚动的内容的美丽模糊效果。
- 在滚动视图上设置背景将导致 Large Title NavigationView 停止折叠。
当我不得不更改我正在使用的应用程序的所有屏幕上的背景颜色时,我遇到了这些问题。
所以我做了更多的挖掘和试验,并设法找到了一个很好的解决问题的方法。
这是原始解决方案:
我们将 ScrollView 包装成 2 个几何 readers。
最上面的是关于安全区域的——我们需要这个来阅读安全区域的插图
第二个是全屏。
我们将滚动视图放入第二个几何体中 reader - 使其大小为全屏。
然后我们通过应用安全区域填充,使用 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 扩展。
逻辑基本上是我们将接收器包装到几何体中 readers 并将其内容包装到具有安全区域填充的 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"))
}
}
}
面对同样的问题,我做了一些调查。
故障似乎来自滚动视图弹跳和滚动上下文减速的组合。
现在我已经设法通过将减速率设置为快来使故障消失。它似乎让 swiftui 更好地计算布局,同时保持弹跳动画处于活动状态。
我的解决方法很简单,只需在视图的初始化中设置以下内容即可。缺点是这会影响滚动减速的速度。
init() {
UIScrollView.appearance().decelerationRate = .fast
}
一个可能的改进是计算正在显示的内容的大小,然后根据需要动态切换减速率。
我简化了。这是:
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()
}
}
}
我有一个简单的看法:
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(0..<2) { _ in
CardVew(for: cardData)
}
}
}
.navigationBarTitle("Testing", displayMode: .automatic)
}
}
但是您可以用任何东西替换 CardView - 故障仍然存在。 Glitch video
有办法解决吗?
Xcode12.0.1,Swift5
这是一个解决方法。将 .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)
}
}
}
将顶部填充设置为 1 至少破坏了 2 个主要方面:
- 滚动视图不会在 NavigationView 和 TabView 下扩展 - 这使得它失去了在栏下滚动的内容的美丽模糊效果。
- 在滚动视图上设置背景将导致 Large Title NavigationView 停止折叠。
当我不得不更改我正在使用的应用程序的所有屏幕上的背景颜色时,我遇到了这些问题。 所以我做了更多的挖掘和试验,并设法找到了一个很好的解决问题的方法。
这是原始解决方案:
我们将 ScrollView 包装成 2 个几何 readers。
最上面的是关于安全区域的——我们需要这个来阅读安全区域的插图 第二个是全屏。
我们将滚动视图放入第二个几何体中 reader - 使其大小为全屏。
然后我们通过应用安全区域填充,使用 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 扩展。
逻辑基本上是我们将接收器包装到几何体中 readers 并将其内容包装到具有安全区域填充的 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"))
}
}
}
面对同样的问题,我做了一些调查。 故障似乎来自滚动视图弹跳和滚动上下文减速的组合。
现在我已经设法通过将减速率设置为快来使故障消失。它似乎让 swiftui 更好地计算布局,同时保持弹跳动画处于活动状态。
我的解决方法很简单,只需在视图的初始化中设置以下内容即可。缺点是这会影响滚动减速的速度。
init() {
UIScrollView.appearance().decelerationRate = .fast
}
一个可能的改进是计算正在显示的内容的大小,然后根据需要动态切换减速率。
我简化了
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()
}
}
}