61

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

Jacksonkr
  • 31,583
  • 39
  • 180
  • 284
  • 5
    I know this is old but this solution is awesome for people finding this thread like I did: https://github.com/reydanro/UIImageViewAligned – tommybananas Apr 30 '14 at 21:22

8 Answers8

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.

Community
  • 1
  • 1
Mundi
  • 79,884
  • 17
  • 117
  • 140
14

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
    }

}
Fattie
  • 27,874
  • 70
  • 431
  • 719
Werner Altewischer
  • 10,080
  • 4
  • 53
  • 60
14

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

Daniel Storm
  • 18,301
  • 9
  • 84
  • 152
protspace
  • 2,047
  • 1
  • 25
  • 30
11

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
noripcord
  • 3,412
  • 5
  • 29
  • 26
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

Łukasz Duda
  • 169
  • 1
  • 4
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.
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.

David T
  • 2,724
  • 1
  • 17
  • 27
-3

If you are able to subclass UIImageView, then you can just override the image var.

override var image: UIImage? {
    didSet {
        self.sizeToFit()
    }
}

In Objective-C you can do the same thing by overriding the setter.

Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421