考虑以下示例:
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
视图,因此每次都会重建视图(并重新加载图像)。我认为这就是白色闪光的来源,因为您在动画中间看到灰色占位符。如果您可以将滚动视图保留在适当的位置,可能在不需要时禁用滚动(如果可能的话),那么操作系统可以保留 AsyncImage
的标识。 (参见https://developer.apple.com/videos/play/wwdc2021/10022/)
这就引出了您要调查的第二个领域。
AsyncImage
的美妙之处在于它为您从网络加载内容提供了便利。不幸的是,它并没有使通信速度更快。您的目标应该是让 AsyncImage
尽可能少地访问网络。
现在,您的调整大小策略侧重于调整图像大小。这意味着对于每次转换,您都在“访问网络”(请阅读将代码放在缓慢、尘土飞扬的土路上)。您应该只加载一次图像(缓慢的部分)并调整显示它的视图的大小,而不是调整图像的大小。总体思路是让
AsyncImage
加载图像,然后通过动画视图框架来控制图像的动画效果。
这是我帮不上忙的地方。我对
AsyncImage
的了解还不够,不知道它是否有能力实施该策略。似乎应该是……但我不知道是不是。您可能必须下载图像并将其存储为与呈现图像的视图分开的状态。
所以我的建议是限制
AsyncImage
重新加载网络数据的次数。这涉及帮助 SwiftUI 维护 AsyncImage
的标识,这样它就不必在每次创建视图时重新加载。并且,尝试在视图而不是图像上实现动画和缩放,因为重新缩放图像还需要重新加载网络。
将matchedGeometryEffect与AsyncImage一起使用可能具有挑战性,特别是当图像长宽比在动画过程中发生变化时。我用来解决这个问题的一个可靠方法是将 AsyncImage 覆盖在 Rectangle 上,并将 matchesGeometryEffect 应用于 Rectangle。
确保图像可调整大小,然后在 .scaleToFit() 或 .scaleToFill() 之间进行选择。然后通过修改矩形本身来控制图像的位置和大小。
如果你想要.scaleToFill(),那么你必须在应用matchedGeometryEffect修改器之前裁剪矩形。
ZStack {
if showSplash {
GeometryReader { geometry in
Rectangle()
.fill(.gray)
.overlay {
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image
.resizable()
.scaledToFill()
} placeholder: {
Color.clear
}
}
.clipped()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.frame(width: geometry.size.width)
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
toggleSplashScreen(false)
}
} else {
ScrollView {
GeometryReader { geometry in
Rectangle()
.fill(.gray)
.edgesIgnoringSafeArea(.all)
.overlay {
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image
.resizable()
.scaledToFill()
} placeholder: {
Color.clear
}
}
.clipped()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.frame(width: geometry.size.width, height: 400)
}
.onTapGesture {
toggleSplashScreen(true)
}
}
}
}