I'm trying to create an AsyncImage
with a placeholder image. With the following constraints...
AsyncImage
.AsyncImage
.AsyncImage
is fixed.The requirements...
AsyncImage
.AsyncImage
(set by the aspect ratio).In my head this should be simple. 😅
But I just cannot get this to work without at least one of the requirements breaking.
This is the closest I've got so far but the AsyncImage
is just not clipped so overflows beyond either the height or width.
You can paste this into Xcode and it will show the
import SwiftUI
let aspect = 1.5
struct TestView: View {
var body: some View {
AsyncImage(
url: URL(string: "https://placekitten.com/1920/1080"),
transaction: .init(animation: .easeIn(duration: 3)) // <- slow animation to show issue
) { phase in
switch phase {
case .success(let image):
image
.resizable()
.scaledToFill()
.aspectRatio(aspect, contentMode: .fill)
.clipped()
default:
Image(systemName: "rectangle")
.resizable()
.scaledToFill()
.aspectRatio(aspect, contentMode: .fill)
.clipped()
.foregroundColor(.white)
}
}
.aspectRatio(aspect, contentMode: .fill)
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
VStack {
TestView()
.overlay {
// This overlay shows the correct rect but the image is just not cclipped.
Color.black
.opacity(0.5)
.aspectRatio(aspect, contentMode: .fit)
}
.padding()
.aspectRatio(aspect, contentMode: .fit)
}
.background(Color.red)
.padding()
}
}
This gives me the following...
The red background is from the VStack. The black transparent overlay shows the aspect ratio I'm looking for based on the aspect
set at the top of the file.
OK... I kept battling on with this and found an answer that doesn't require GeometryReader
.
I created this extension on View
.
extension View {
public func framedAspectRatio(_ aspect: CGFloat? = nil, contentMode: ContentMode) -> some View where Self == Image {
self.resizable()
.fixedAspectRatio(contentMode: contentMode)
.allowsHitTesting(false)
}
public func fixedAspectRatio(_ aspect: CGFloat? = nil, contentMode: ContentMode) -> some View {
self.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.aspectRatio(aspect, contentMode: contentMode)
.clipped()
}
}
And now with this code...
struct TestView: View {
var body: some View {
AsyncImage(
url: URL(string: "https://placehold.co/300x1600/png"),
transaction: .init(animation: .easeIn(duration: 3)) // <- slow animation to show issue
) { phase in
switch phase {
case .success(let image):
image.framedAspectRatio(contentMode: .fill)
default:
Image(systemName: "checkmark.rectangle")
.clipped()
.foregroundColor(.white)
}
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text("Fit")
TestView()
.opacity(0.8)
.fixedAspectRatio(1, contentMode: .fit)
.frame(width: 100, height: 200)
.background(Color.black)
.padding()
Text("Fill")
TestView()
.opacity(0.8)
.fixedAspectRatio(1, contentMode: .fill)
.frame(width: 100, height: 200)
.background(Color.black)
.padding()
}
}
}
Everything works as expected.