I want my UIImageView
to grow or shrink depending on the size of what the actual image it\'s displaying is. But I want it to stay vertically centered and 10pts
If anyone comes here wanting a simple copy+paste for Xamarin heres @algal's code (with some of @alexander's changes) converted to .Net
Also I've changed it to constrain the Height based on the Width + Aspect Ratio as that fit all of my use cases.
/// <summary>
/// UIImage view that will set the height correct based on the current
/// width and the aspect ratio of the image.
/// https://stackoverflow.com/a/42654375/9829321
/// </summary>
public class UIAspectFitImageView : UIImageView
{
private NSLayoutConstraint _heightConstraint;
public override UIImage Image
{
get => base.Image;
set
{
base.Image = value;
UpdateAspectRatioConstraint();
}
}
public UIAspectFitImageView()
=> Setup();
public UIAspectFitImageView(UIImage image) : base(image)
=> Setup();
public UIAspectFitImageView(NSCoder coder) : base(coder)
=> Setup();
public UIAspectFitImageView(CGRect frame) : base(frame)
=> Setup();
public UIAspectFitImageView(UIImage image, UIImage highlightedImage) : base(image, highlightedImage)
=> Setup();
private void Setup()
{
ContentMode = UIViewContentMode.ScaleAspectFit;
UpdateAspectRatioConstraint();
}
private void UpdateAspectRatioConstraint()
{
if (Image != null && Image.Size.Width != 0)
{
var aspectRatio = Image.Size.Height / Image.Size.Width;
if (_heightConstraint?.Multiplier != aspectRatio)
{
if (_heightConstraint != null)
{
RemoveConstraint(_heightConstraint);
_heightConstraint.Dispose();
_heightConstraint = null;
}
_heightConstraint = HeightAnchor.ConstraintEqualTo(WidthAnchor, aspectRatio);
_heightConstraint.Priority = 1000;
_heightConstraint.Active = true;
AddConstraint(_heightConstraint);
}
}
}
}
I usually add no image height and width constraints with value 0
and priority
less than UIImageView
's contentCompressionResistancePriority
, which by default is 750
. These two constraints will activate only when there is no image, and will take care of ambiguous constraints warnings both at run time and compile time.
Pretty straight forward to add these in IB. Programatically, it should look something like below.
let noImageWidthConstraint = imageview.widthAnchor.constraint(equalToConstant: 0)
noImageWidthConstraint.priority = UILayoutPriority(rawValue: 100)
noImageWidthConstraint.isActive = true
let noImageHeightConstraint = imageview.heightAnchor.constraint(equalToConstant: 0)
noImageHeightConstraint.priority = UILayoutPriority(rawValue: 100)
noImageHeightConstraint.isActive = true
Here is an AspectFit
UIImageView
-derived implementation that works with Auto Layout when only one dimension is constrained. The other one will be set automatically. It will keep image aspect ratio and doesn't add any margins around the image.
It's tweaked Objective-C implementation of @algal idea with the following differences:
priority = (UILayoutPriorityDefaultLow +
UILayoutPriorityFittingSizeLevel) / 2.0
which evaluates to 150
isn't enough to beat the priority of 1000
of the default image
content size constraint. So aspect constraint priority was increased
to 1000
as well.image
setter could be called multiple times, so it's a good idea to not cause extra layout calculations here.required public init?(coder
aDecoder: NSCoder)
and public override init(frame:CGRect)
, so
these two are taken out. They aren't overridden by UIImageView
,
that's why there is no point of adding the aspect constraint until an image is set.AspectKeepUIImageView.h
:
NS_ASSUME_NONNULL_BEGIN
@interface AspectKeepUIImageView : UIImageView
- (instancetype)initWithImage:(nullable UIImage *)image;
- (instancetype)initWithImage:(nullable UIImage *)image highlightedImage:(nullable UIImage *)highlightedImage;
@end
NS_ASSUME_NONNULL_END
AspectKeepUIImageView.m
:
#import "AspectKeepUIImageView.h"
@implementation AspectKeepUIImageView
{
NSLayoutConstraint *_aspectContraint;
}
- (instancetype)initWithImage:(nullable UIImage *)image
{
self = [super initWithImage:image];
[self initInternal];
return self;
}
- (instancetype)initWithImage:(nullable UIImage *)image highlightedImage:(nullable UIImage *)highlightedImage
{
self = [super initWithImage:image highlightedImage:highlightedImage];
[self initInternal];
return self;
}
- (void)initInternal
{
self.contentMode = UIViewContentModeScaleAspectFit;
[self updateAspectConstraint];
}
- (void)setImage:(UIImage *)image
{
[super setImage:image];
[self updateAspectConstraint];
}
- (void)updateAspectConstraint
{
CGSize imageSize = self.image.size;
CGFloat aspectRatio = imageSize.height > 0.0f
? imageSize.width / imageSize.height
: 0.0f;
if (_aspectContraint.multiplier != aspectRatio)
{
[self removeConstraint:_aspectContraint];
_aspectContraint =
[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeHeight
multiplier:aspectRatio
constant:0.f];
_aspectContraint.priority = UILayoutPriorityRequired;
[self addConstraint:_aspectContraint];
}
}
@end
For @algal case 3, all I had to do was
class ScaledHeightImageView: UIImageView {
override var intrinsicContentSize: CGSize {
if let myImage = self.image {
let myImageWidth = myImage.size.width
let myImageHeight = myImage.size.height
let myViewWidth = self.frame.size.width
let ratio = myViewWidth/myImageWidth
let scaledHeight = myImageHeight * ratio
return CGSize(width: myViewWidth, height: scaledHeight)
}
return CGSize(width: -1.0, height: -1.0)
}
}
This scales the height component of the intrinsicContentSize. (-1.0, -1.0) is returned when there is no image because this is the default behavior in the superclass.
Also, I did not need to set UITableViewAutomaticDimension
for the table with the cell containing the image view, and the cell still sizes automatically. The image view's content mode is Aspect Fit
.
I used @algal's excellent answer (case #3) and improved it for my use case.
I wanted to be able to set min and max ratio constraints. So if I have a minimum ratio constraint of 1.0 and set a picture that is higher than wide, the size should be square.
Also it should fill the UIImageView
in those cases.
This works even in Interface Builder. Just add a UIImageView
, set RatioBasedImageView
as custom class in the identity inspector and set the max and min aspect ratios in the attributes inspector.
Here's my code.
@IBDesignable
public class RatioBasedImageView : UIImageView {
/// constraint to maintain same aspect ratio as the image
private var aspectRatioConstraint:NSLayoutConstraint? = nil
// This makes it use the correct size in Interface Builder
public override func prepareForInterfaceBuilder() {
invalidateIntrinsicContentSize()
}
@IBInspectable
var maxAspectRatio: CGFloat = 999 {
didSet {
updateAspectRatioConstraint()
}
}
@IBInspectable
var minAspectRatio: CGFloat = 0 {
didSet {
updateAspectRatioConstraint()
}
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
self.setup()
}
public override init(frame:CGRect) {
super.init(frame:frame)
self.setup()
}
public override init(image: UIImage!) {
super.init(image:image)
self.setup()
}
public override init(image: UIImage!, highlightedImage: UIImage?) {
super.init(image:image,highlightedImage:highlightedImage)
self.setup()
}
override public var image: UIImage? {
didSet { self.updateAspectRatioConstraint() }
}
private func setup() {
self.updateAspectRatioConstraint()
}
/// Removes any pre-existing aspect ratio constraint, and adds a new one based on the current image
private func updateAspectRatioConstraint() {
// remove any existing aspect ratio constraint
if let constraint = self.aspectRatioConstraint {
self.removeConstraint(constraint)
}
self.aspectRatioConstraint = nil
if let imageSize = image?.size, imageSize.height != 0 {
var aspectRatio = imageSize.width / imageSize.height
aspectRatio = max(minAspectRatio, aspectRatio)
aspectRatio = min(maxAspectRatio, aspectRatio)
let constraint = NSLayoutConstraint(item: self, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .height,
multiplier: aspectRatio, constant: 0)
constraint.priority = .required
self.addConstraint(constraint)
self.aspectRatioConstraint = constraint
}
}
}
The image view's intrinsic size is already dependent on the size of the image. Your assumptions (and constraints are correct).
However, if you've set up your image view in interface builder and have not provided it with an image, then the layout system (interface builder) won't know how big your image view is supposed to be at compile time. Your layout is ambiguous because your image view could be many sizes. This is what throws the errors.
Once you set your image view's image property, then the image view's intrinsic size is defined by the size of the image. If you're setting the view at runtime, then you can do exactly what Anna mentioned and provide interface builder with a "placeholder" intrinsic size in the property inspector of the image view. This tells interface builder, "use this size for now, I'll give you a real size later". The placeholder constraints are ignored at runtime.
Your other option is to assign the image to the image view in interface builder directly (but I assume your images are dynamic, so this won't work for you).