UIImage aspect fit and align to top
Asked Answered
G

7

65

It looks like aspect fit aligns the image to the bottom of the frame by default. Is there a way to override the alignment while keeping aspect fit intact?

** EDIT **

This question predates auto layout. In fact, auto layout was being revealed in WWDC 2012 the same week this question was asked

Gellman answered 14/6, 2012 at 19:22 Comment(1)
I know this is old but this solution is awesome for people finding this thread like I did: github.com/reydanro/UIImageViewAlignedAvalos
L
24

In short, you cannot do this with a UIImageView.

One solution is to subclass a UIView containing an UIImageView and change its frame according to image size. For example, you can find one version here.

Landel answered 15/6, 2012 at 0:0 Comment(2)
If you can override UIImageView (as I do below) you can do it in one shotAoudad
Not quite the same effect OP needed.Landel
T
15

The way to do this is to modify the contentsRect of the UIImageView layer. The following code from my project (sub class of UIImageView) assumes scaleToFill and offsets the image such that it aligns top, bottom, left or right instead of the default center alignment. For aspectFit is would be a similar solution.

typedef NS_OPTIONS(NSUInteger, AHTImageAlignmentMode) {
    AHTImageAlignmentModeCenter = 0,
    AHTImageAlignmentModeLeft = 1 << 0,
    AHTImageAlignmentModeRight = 1 << 1,
    AHTImageAlignmentModeTop = 1 << 2,
    AHTImageAlignmentModeBottom = 1 << 3,
    AHTImageAlignmentModeDefault = AHTImageAlignmentModeCenter,
};

- (void)updateImageViewContentsRect {
    CGRect imageViewContentsRect = CGRectMake(0, 0, 1, 1);

    if (self.image.size.height > 0 && self.bounds.size.height > 0) {

        CGRect imageViewBounds = self.bounds;
        CGSize imageSize = self.image.size;

        CGFloat imageViewFactor = imageViewBounds.size.width / imageViewBounds.size.height;
        CGFloat imageFactor = imageSize.width / imageSize.height;

        if (imageFactor > imageViewFactor) {
            //Image is wider than the view, so height will match
            CGFloat scaledImageWidth = imageViewBounds.size.height * imageFactor;
            CGFloat xOffset = 0.0;
            if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeLeft)) {
                xOffset = -(scaledImageWidth - imageViewBounds.size.width) / 2;
            } else if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeRight)) {
                xOffset = (scaledImageWidth - imageViewBounds.size.width) / 2;
            }
            imageViewContentsRect.origin.x = (xOffset / scaledImageWidth);
        } else if (imageFactor < imageViewFactor) {
            CGFloat scaledImageHeight = imageViewBounds.size.width / imageFactor;

            CGFloat yOffset = 0.0;
            if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeTop)) {
                yOffset = -(scaledImageHeight - imageViewBounds.size.height) / 2;
            } else if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeBottom)) {
                yOffset = (scaledImageHeight - imageViewBounds.size.height) / 2;
            }
            imageViewContentsRect.origin.y = (yOffset / scaledImageHeight);
        }
    }
    self.layer.contentsRect = imageViewContentsRect;
}

Swift version

///Works perfectly with AspectFill. Note there's no protection for obscure issues like (say) scaledImageWidth is zero.
class AlignmentImageView: UIImageView {

    enum HorizontalAlignment { case left, center, right }
    enum VerticalAlignment { case top, center, bottom }

    var horizontalAlignment: HorizontalAlignment = .center  { didSet { updateContentsRect() } }
    var verticalAlignment: VerticalAlignment = .center  { didSet { updateContentsRect() } }

    override var image: UIImage? { didSet { updateContentsRect() } }

    override func layoutSubviews() {
        super.layoutSubviews()
        updateContentsRect()
    }

    private func updateContentsRect() {
        var contentsRect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))

        guard let imageSize = image?.size else {
            layer.contentsRect = contentsRect
            return
        }

        let viewBounds = bounds
        let imageViewFactor = viewBounds.size.width / viewBounds.size.height
        let imageFactor = imageSize.width / imageSize.height

        if imageFactor > imageViewFactor {
            // Image is wider than the view, so height will match
            let scaledImageWidth = viewBounds.size.height * imageFactor
            var xOffset: CGFloat = 0.0

            if case .left = horizontalAlignment {
                xOffset = -(scaledImageWidth - viewBounds.size.width) / 2
            }
            else if case .right = horizontalAlignment {
                xOffset = (scaledImageWidth - viewBounds.size.width) / 2
            }

            contentsRect.origin.x = xOffset / scaledImageWidth
        }
        else {
            let scaledImageHeight = viewBounds.size.width / imageFactor
            var yOffset: CGFloat = 0.0

            if case .top = verticalAlignment {
                yOffset = -(scaledImageHeight - viewBounds.size.height) / 2
            }
            else if case .bottom = verticalAlignment {
                yOffset = (scaledImageHeight - viewBounds.size.height) / 2
            }

            contentsRect.origin.y = yOffset / scaledImageHeight
        }

        layer.contentsRect = contentsRect
    }

}
Touchstone answered 21/7, 2016 at 11:7 Comment(0)
H
14

Set the UIImageView's bottom layout constraint priority to lowest (i.e. 250) and it will handle it for you.

Hedveh answered 22/5, 2017 at 9:54 Comment(1)
This worked for me thanks @Hedveh (ios13.4 Xcode 11.3)Purpura
P
12

this will make the image fill the width and occupy only the height it needs to fit the image (widthly talking)

swift 4.2:

let image = UIImage(named: "my_image")!
let ratio = image.size.width / image.size.height
cardImageView.widthAnchor
  .constraint(equalTo: cardImageView.heightAnchor, multiplier: ratio).isActive = true
Partridgeberry answered 7/10, 2019 at 3:58 Comment(0)
D
2

I had similar problem. Simplest way was to create own subclass of UIImageView. I add for subclass 3 properties so now it can be use easly without knowing internal implementation:

@property (nonatomic) LDImageVerticalAlignment imageVerticalAlignment;
@property (nonatomic) LDImageHorizontalAlignment imageHorizontalAlignment;
@property (nonatomic) LDImageContentMode imageContentMode;

You can check it here: https://github.com/LucasssD/LDAlignmentImageView

Disaccustom answered 3/2, 2017 at 21:37 Comment(0)
A
2
  1. Add the Aspect Ratio constraint with your image proportions.
  2. Do not pin UIImageView to bottom.
  3. If you want to change the UIImage dynamically remember to update aspect ratio constraint.
Adaiha answered 3/10, 2018 at 12:31 Comment(0)
H
0

I solved this natively in Interface Builder by setting a constraint on the height of the UIImageView, since the image would always be 'pushed' up when the image was larger than the screen size.

More specifically, I set the UIImageView to be the same height as the View it is in (via height constraint), then positioned the UIImageView with spacing constraints in IB. This results in the UIImageView having an 'Aspect Fit' which still respects the top spacing constraint I set in IB.

Hills answered 7/11, 2016 at 12:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.