iosswiftuiimageviewcontentmode

UIImage content mode aspectFit and bottom


Is it possible to set the contentMode for my UIImage to .scaleAspectFit and .bottom simultaneously ?

This is how my image looks like at the moment:

enter image description here

UIImageView:

let nightSky: UIImageView = {
    let v = UIImageView()
    v.image = UIImage(named: "nightSky")
    v.translatesAutoresizingMaskIntoConstraints = false
    v.contentMode = .scaleAspectFit
    return v
}()

Constraints:

nightSky.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        nightSky.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -120).isActive = true
        nightSky.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
        nightSky.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true

Solution

  • Here is a custom class that allows Aspect Fit and Alignment properties.

    It is marked @IBDesignable so you can see it in Storyboard / Interface Builder.

    The @IBInspectable properties are:

    enter image description here

    Select the image as you would for a normal UIImageView.

    Valid values for HAlign are "left" "center" "right" or leave blank for default (center).

    Valid values for VAlign are "top" "center" "bottom" or leave blank for default (center).

    "Aspect Fill" is On or Off (True/False). If True, the image will be scaled to Aspect Fill instead of Aspect Fit.

    @IBDesignable
    class AlignedAspectFitImageView: UIView {
        
        enum HorizontalAlignment: String {
            case left, center, right
        }
        
        enum VerticalAlignment: String {
            case top, center, bottom
        }
        
        private var theImageView: UIImageView = {
            let v = UIImageView()
            return v
        }()
        
        @IBInspectable var image: UIImage? {
            get { return theImageView.image }
            set {
                theImageView.image = newValue
                setNeedsLayout()
            }
        }
        
        @IBInspectable var hAlign: String = "center" {
            willSet {
                // Ensure user enters a valid alignment name while making it lowercase.
                if let newAlign = HorizontalAlignment(rawValue: newValue.lowercased()) {
                    horizontalAlignment = newAlign
                }
            }
        }
        
        @IBInspectable var vAlign: String = "center" {
            willSet {
                // Ensure user enters a valid alignment name while making it lowercase.
                if let newAlign = VerticalAlignment(rawValue: newValue.lowercased()) {
                    verticalAlignment = newAlign
                }
            }
        }
        
        @IBInspectable var aspectFill: Bool = false {
            didSet {
                setNeedsLayout()
            }
        }
        
        var horizontalAlignment: HorizontalAlignment = .center
        var verticalAlignment: VerticalAlignment = .center
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        override func prepareForInterfaceBuilder() {
            super.prepareForInterfaceBuilder()
            commonInit()
        }
        func commonInit() -> Void {
            clipsToBounds = true
            addSubview(theImageView)
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            guard let img = theImageView.image else {
                return
            }
            
            var newRect = bounds
            
            let viewRatio = bounds.size.width / bounds.size.height
            let imgRatio = img.size.width / img.size.height
            
            // if view ratio is equal to image ratio, we can fill the frame
            if viewRatio == imgRatio {
                theImageView.frame = newRect
                return
            }
            
            // otherwise, calculate the desired frame
    
            var calcMode: Int = 1
            if aspectFill {
                calcMode = imgRatio > 1.0 ? 1 : 2
            } else {
                calcMode = imgRatio < 1.0 ? 1 : 2
            }
    
            if calcMode == 1 {
                // image is taller than wide
                let heightFactor = bounds.size.height / img.size.height
                let w = img.size.width * heightFactor
                newRect.size.width = w
                switch horizontalAlignment {
                case .center:
                    newRect.origin.x = (bounds.size.width - w) * 0.5
                case .right:
                    newRect.origin.x = bounds.size.width - w
                default: break  // left align - no changes needed
                }
            } else {
                // image is wider than tall
                let widthFactor = bounds.size.width / img.size.width
                let h = img.size.height * widthFactor
                newRect.size.height = h
                switch verticalAlignment {
                case .center:
                    newRect.origin.y = (bounds.size.height - h) * 0.5
                case .bottom:
                    newRect.origin.y = bounds.size.height - h
                default: break  // top align - no changes needed
                }
            }
    
            theImageView.frame = newRect
        }
    }
    

    Using this image:

    enter image description here

    Here's how it looks with a 240 x 240 AlignedAspectFitImageView with background color set to yellow (so we can see the frame):

    enter image description here

    Properties can also be set via code. For example:

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let testImageView = AlignedAspectFitImageView()
        testImageView.image = UIImage(named: "bkg640x360")
        testImageView.verticalAlignment = .bottom
        
        view.addSubview(testImageView)
    
        // set frame / constraints / etc
        testImageView.frame = CGRect(x: 40, y: 40, width: 240, height: 240)
    }
    

    To show the difference between "Aspect Fill" and "Aspect Fit"...

    Using this image:

    enter image description here

    We get this result with Aspect Fill: Off and VAlign: bottom:

    enter image description here

    and then this result with Aspect Fill: On and HAlign: right:

    enter image description here