使用 AsyncImage 匹配几何效果 iOS 15
Matched Geometry Effect with AsyncImage iOS 15
考虑以下示例:
struct ContentView: View {
@State var showSplash: Bool = true
@Namespace var animationNamespace
var body: some View {
ZStack {
if showSplash {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
.frame(width: geometry.size.width)
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.all)
.clipped()
} placeholder: {
Color.gray
}
}
.onTapGesture {
toggleSplashScreen(false)
}
} else {
ScrollView {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image
image
.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
} placeholder: {
Color.gray
}
.frame(width: geometry.size.width, height: 400)
.clipped()
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
toggleSplashScreen(true)
}
}
}
}
}
}
这里有辅助方法:
private extension ContentView {
func toggleSplashScreen(_ toggle: Bool) {
withAnimation(.spring(response: 0.85, dampingFraction: 0.95)) {
showSplash = toggle
}
}
}
这会产生:
我注意到这里有两件事我想解决
- 两种状态转换时的闪烁白色效果
- 我注意到,因为我们正在使用
AsyncImage
,当 showSplash
更改 AsyncImages 时,有时只会命中 placeholder
块。结果,过渡变得非常不稳定。我用资产文件中的静态图像对此进行了测试,然后过渡变得平滑。我还尝试在 AsyncImage 上创建一个缓存机制,但有时仍然遇到 placeholder
块问题。
很想听听任何想法 :) 谢谢!
我认为您可以做几件事来改进这一点。
首先,您有点反对 SwiftUI 维护视图身份的方式。 SwiftUI 确定何时可以重用现有结构而不是重新创建结构的方法之一是它在视图层次结构中的位置。因此,当您切换结构时,您会从:
GeometryReader
AsyncImage
至
ScrollView
GeometryReader
AsyncImage
因此,系统认为这是两个 AsyncImage
视图,因此每次都会重建视图(并重新加载图像)。我认为这就是白色闪光的来源,因为您在动画中间看到了灰色占位符。如果您可以保留滚动视图,可能在不需要时禁用滚动(如果可能的话),那么 OS 可以保持 AsyncImage
的身份。 (参见 https://developer.apple.com/videos/play/wwdc2021/10022/)
这将引导您进入第二个调查领域。 AsyncImage
非常棒,因为它为您提供了从网络加载内容的便利。不幸的是,它并没有使这种沟通更快。您的目标应该是 AsyncImage
访问网络的次数尽可能少。
现在,您的调整策略主要集中在调整图像大小上。这意味着对于每次转换,您都在“访问网络”(阅读将您的代码放在缓慢、多尘、泥泞的道路上)。您应该只加载一次图像(较慢的部分)并调整显示它的视图的大小,而不是调整图像的大小。一般的想法是让 AsyncImage
加载图像,然后通过动画视图的框架来控制图像的动画方式。
这是我得到的帮助较少的地方。我对 AsyncImage
的了解还不够,无法知道它是否能够实施该策略。似乎应该是……但我不知道是不是。您可能不得不求助于将图像下载和存储为与呈现它的视图分开的状态。
所以我的建议是限制 AsyncImage
必须重新加载网络数据的次数。这涉及帮助 SwiftUI 维护 AsyncImage
的身份,因此它不必在每次创建视图时都重新加载。并且,尝试在视图而不是图像上实现动画和缩放,因为重新缩放图像也需要重新加载网络。
考虑以下示例:
struct ContentView: View {
@State var showSplash: Bool = true
@Namespace var animationNamespace
var body: some View {
ZStack {
if showSplash {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
.frame(width: geometry.size.width)
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.all)
.clipped()
} placeholder: {
Color.gray
}
}
.onTapGesture {
toggleSplashScreen(false)
}
} else {
ScrollView {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image
image
.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
} placeholder: {
Color.gray
}
.frame(width: geometry.size.width, height: 400)
.clipped()
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
toggleSplashScreen(true)
}
}
}
}
}
}
这里有辅助方法:
private extension ContentView {
func toggleSplashScreen(_ toggle: Bool) {
withAnimation(.spring(response: 0.85, dampingFraction: 0.95)) {
showSplash = toggle
}
}
}
这会产生:
我注意到这里有两件事我想解决
- 两种状态转换时的闪烁白色效果
- 我注意到,因为我们正在使用
AsyncImage
,当showSplash
更改 AsyncImages 时,有时只会命中placeholder
块。结果,过渡变得非常不稳定。我用资产文件中的静态图像对此进行了测试,然后过渡变得平滑。我还尝试在 AsyncImage 上创建一个缓存机制,但有时仍然遇到placeholder
块问题。
很想听听任何想法 :) 谢谢!
我认为您可以做几件事来改进这一点。
首先,您有点反对 SwiftUI 维护视图身份的方式。 SwiftUI 确定何时可以重用现有结构而不是重新创建结构的方法之一是它在视图层次结构中的位置。因此,当您切换结构时,您会从:
GeometryReader
AsyncImage
至
ScrollView
GeometryReader
AsyncImage
因此,系统认为这是两个 AsyncImage
视图,因此每次都会重建视图(并重新加载图像)。我认为这就是白色闪光的来源,因为您在动画中间看到了灰色占位符。如果您可以保留滚动视图,可能在不需要时禁用滚动(如果可能的话),那么 OS 可以保持 AsyncImage
的身份。 (参见 https://developer.apple.com/videos/play/wwdc2021/10022/)
这将引导您进入第二个调查领域。 AsyncImage
非常棒,因为它为您提供了从网络加载内容的便利。不幸的是,它并没有使这种沟通更快。您的目标应该是 AsyncImage
访问网络的次数尽可能少。
现在,您的调整策略主要集中在调整图像大小上。这意味着对于每次转换,您都在“访问网络”(阅读将您的代码放在缓慢、多尘、泥泞的道路上)。您应该只加载一次图像(较慢的部分)并调整显示它的视图的大小,而不是调整图像的大小。一般的想法是让 AsyncImage
加载图像,然后通过动画视图的框架来控制图像的动画方式。
这是我得到的帮助较少的地方。我对 AsyncImage
的了解还不够,无法知道它是否能够实施该策略。似乎应该是……但我不知道是不是。您可能不得不求助于将图像下载和存储为与呈现它的视图分开的状态。
所以我的建议是限制 AsyncImage
必须重新加载网络数据的次数。这涉及帮助 SwiftUI 维护 AsyncImage
的身份,因此它不必在每次创建视图时都重新加载。并且,尝试在视图而不是图像上实现动画和缩放,因为重新缩放图像也需要重新加载网络。