UIImage content mode aspectFit and bottom

1.9k Views Asked by At

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
2

There are 2 best solutions below

3
On BEST ANSWER

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:

  • Image
  • Horizontal Alignment
  • Vertical Alignment
  • Aspect Fill

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

0
On

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