2

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
Chris
  • 1,828
  • 6
  • 40
  • 108
  • You can only have one `contentMode`. If you have this image stored locally, then you should edit the actual image. If you are loading an image via a network call that you have no control over, then you will probably need to compromise somewhere unless you want to go down the route of algorithmic cropping with `CGContext`, which gets very expensive with larger images or multiple images. – Jeremy H Apr 10 '20 at 15:09
  • @JeremyH I have it stored locally. But how should I edit it? – Chris Apr 10 '20 at 15:10
  • Jump into Photoshop or equivalent, and crop it so there's no padding on the bottom. Or you can add padding to the top of the image by increasing the canvas height. – Jeremy H Apr 10 '20 at 15:11
  • @JeremyH there is no padding in the actual image – Chris Apr 10 '20 at 15:12
  • @JeremyH do I constraint it wrong maybe ? – Chris Apr 10 '20 at 15:12
  • Perhaps. I don't know the bigger picture of what you're trying to do, so I don't have enough context to say. What I can say is I don't use `centerYAnchor` unless I have assurance the parent container will be more than enough in size for the element in question to comfortably when running the app on various devices and orientations. – Jeremy H Apr 10 '20 at 15:15
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/211377/discussion-between-chris-and-jeremy-h). – Chris Apr 10 '20 at 15:37

2 Answers2

2

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

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Any chance that I can refactor it for aspectToFill? – oğuz Feb 17 '22 at 13:31
  • @EdwardMordrake - you can get "Aspect Fill" by changing `if imgRatio < 1.0` to `if imgRatio > 1.0` ... I edited my answer with an updated class that now includes `aspectFill` as a bool `@IBInspectable` property. – DonMag Feb 17 '22 at 14:52
  • Thank you for your updated answer, it worked like a charm! Now I need to inspect and understand how is it working. – oğuz Feb 18 '22 at 05:53
0

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

ammar shahid
  • 411
  • 4
  • 17