I need to display some asynchronously loaded images in a view, and they can vary from very small (say 20x20) to huge (2000+).
Getting the large images to fit the view was pretty easy:
AsyncImage(
url: image.thumb,
content: { image in
image.resizable()
.aspectRatio(contentMode: .fit)
},
placeholder: {
ProgressView()
}
)
… but that’s stretching the smaller images to fit the view as well.
How can I say something to the effect of “please show this image at its original size, unless larger than the view, in which case clamp it"? (In CSS, something like img { maxWidth: 100%; height: auto; }
)
I need to display some asynchronously loaded images in a view, and they can vary from very small (say 20x20) to huge (2000+).
Getting the large images to fit the view was pretty easy:
AsyncImage(
url: image.thumb,
content: { image in
image.resizable()
.aspectRatio(contentMode: .fit)
},
placeholder: {
ProgressView()
}
)
… but that’s stretching the smaller images to fit the view as well.
How can I say something to the effect of “please show this image at its original size, unless larger than the view, in which case clamp it"? (In CSS, something like img { maxWidth: 100%; height: auto; }
)
1 Answer
Reset to default 0The answer to SwiftUI scale image only if it is bigger shows a way of preventing a regular Image
from being scaled up when scaling to fit (it was my answer). The idea is to use ViewThatFits
, supplying the unscaled image first, then the image that is scaled-to-fit.
In the case of an AsyncImage
, ViewThatFits
can be used inside the content closure. However, I found that it only works if you set a max width and height on ViewThatFits
. Also, I found that even if .fixedSize()
is applied to ViewThatFits
and to AsyncImage
, the frame size was still larger than it needed to be.
So, an alternative approach is to use a custom Layout
.
Here is a custom Layout
that scales down to fit, but does not scale up. It works as follows:
- Only one child subview is expected.
- The ideal size of the subview is found at the cache stage.
- The (private) function
sizeForProposal
determines the scaled size that will fit inside the proposed size. - The function
sizeThatFits
simply delegates tosizeForProposal
. - The function
placeSubviews
applies the size delivered bysizeForProposal
.
EDIT Added max width and height as parameters too. This provides a more convenient way of enforcing maximum sizes without having to use .frame
and .fixedSize
modifiers (which do not always give the desired result).
struct ScaledDownToFit: Layout {
typealias Cache = CGSize
var maxWidth: CGFloat?
var maxHeight: CGFloat?
func makeCache(subviews: Subviews) -> CGSize {
subviews.first?.sizeThatFits(.unspecified) ?? .zero
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CGSize) -> CGSize {
cache == .zero ? .zero : sizeForProposal(proposal: proposal, viewSize: cache)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout CGSize) {
if let firstSubview = subviews.first, cache != .zero {
let targetSize = sizeForProposal(proposal: proposal, viewSize: cache)
firstSubview.place(at: bounds.origin, proposal: .init(targetSize))
}
}
private func sizeForProposal(proposal: ProposedViewSize, viewSize: CGSize) -> CGSize {
let aspectRatio = viewSize.width / viewSize.height
let minWidth = min(min(viewSize.width, maxWidth ?? .infinity), proposal.width ?? .infinity)
let minHeight = min(min(viewSize.height, maxHeight ?? .infinity), proposal.height ?? .infinity)
let w: CGFloat
let h: CGFloat
if minWidth / minHeight < aspectRatio {
w = minWidth
h = minWidth / aspectRatio
} else {
w = minHeight * aspectRatio
h = minHeight
}
return CGSize(width: w, height: h)
}
}
You might find it convenient to define a View
extension too:
extension View {
func scaledDownToFit(maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> some View {
ScaledDownToFit(maxWidth: maxWidth, maxHeight: maxHeight) { self }
}
}
Example use:
AsyncImage(
url: image.thumb,
content: { image in
image
.resizable()
.scaledDownToFit()
},
placeholder: {
ProgressView()
}
)
.border(.red)
.frame(minWidth: CGFloat?, idealWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?, idealHeight:CGFloat?, maxHeight: CGFloat?, alignment: ...)
– workingdog support Ukraine Commented Jan 19 at 2:16