最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

clamp an image's size in SwiftUI - Stack Overflow

programmeradmin2浏览0评论

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; })

Share Improve this question asked Jan 18 at 23:51 Eric_WVGGEric_WVGG 2,9473 gold badges28 silver badges30 bronze badges 1
  • Have you tried using a simple frame, eg: .frame(minWidth: CGFloat?, idealWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?, idealHeight:CGFloat?, maxHeight: CGFloat?, alignment: ...) – workingdog support Ukraine Commented Jan 19 at 2:16
Add a comment  | 

1 Answer 1

Reset to default 0

The 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 to sizeForProposal.
  • The function placeSubviews applies the size delivered by sizeForProposal.

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)

发布评论

评论列表(0)

  1. 暂无评论