With Auto Layout, how do I make a UIImageView's size dynamic depending on the image?

后端 未结 8 454
花落未央
花落未央 2020-11-28 21:57

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

相关标签:
8条回答
  • 2020-11-28 22:13

    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);
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-28 22:15

    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
    
    0 讨论(0)
  • 2020-11-28 22:20

    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:

    • Expression 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.
    • it removes\re-adds the aspect ratio constraint only if necessary. Keep in mind that this is heavy operation so there is no need to do so if aspect ratio is the same. Moreover, image setter could be called multiple times, so it's a good idea to not cause extra layout calculations here.
    • not sure why we need to override 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
    
    0 讨论(0)
  • 2020-11-28 22:24

    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.

    0 讨论(0)
  • 2020-11-28 22:26

    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
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-28 22:32

    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).

    0 讨论(0)
提交回复
热议问题