47
votes

How do I turn a rectangular image view into a circular image view that can hold shape in auto layout without setting width and height restraints? Thereby allowing the imageView to define it’s size, and size bigger and smaller relative to objects around it with leading, trailing, top, and bottom constraints.

I asked a similar question the other day, but I think this might be posed in a more concise way. Thanks so much!

EDIT

Ok, I started over to make this as simple as possible. I have a view named "Cell" and a UIImageView named "dog" within the cell, and that's it. I don't have "unable to simultaneously satisfy constraints" in the console anymore, just two simple views using auto layout. I'm still trying to use this code to round the UIImageView:

profileImageView.layer.cornerRadius = profileImageView.frame.size.width / 2
profileImageView.clipsToBounds = true

Here is the cell constraint setup:

screenshot 1

Here is the profile pic constraint setup:

screenshot 2

Here is the result without the code, no rounding, but nice and square:

screenshot 3

Here is the result with the code to round:

screenshot 4

This makes no sense to me, because without the rounding code the image is square, and with the code it's diamond shaped. If it's square shouldn't it be a circle with no issues?

EDIT 2

Here's what happens when I remove the bottom constraint and add a multiplier of .637 for equal height to superview.

screenshot 5

15
How are you currently making the image view round?Gavin Hope
@GavinHope I'm calling it into a cell in a tableviewcontroller and using: cell.ProfileImageView.layer.cornerRadius = cell.ProfileImageView.frame.size.width / 2 cell.ProfileImageView.clipsToBounds = trueCHM
If your imageview had the Width != Height then you can find solution here: stackoverflow.com/questions/29685055/…lee

15 Answers

90
votes

Unfortunately you cannot do this using cornerRadius and autolayout — the CGLayer is not affected by autolayout, so any change in the size of the view will not change the radius which has been set once causing, as you have noticed, the circle to lose its shape.

You can create a custom subclass of UIImageView and override layoutSubviews in order to set the cornerRadius each time the bounds of the imageview change.

EDIT

An example might look something like this:

class Foo: UIImageView {
    override func layoutSubviews() {
        super.layoutSubviews()

        let radius: CGFloat = self.bounds.size.width / 2.0

        self.layer.cornerRadius = radius
    }
}

And obviously you would have to constrain the Foobar instance's width to be the same as the height (to maintain a circle). You would probably also want to set the Foobar instance's contentMode to UIViewContentMode.ScaleAspectFill so that it knows how to draw the image (this means that the image is likely to be cropped).

41
votes

Setting radius in viewWillLayoutSubviews will solve the problem

override func viewWillLayoutSubviews() {
  super.viewWillLayoutSubviews()
  profileImageView.layer.cornerRadius = profileImageView.frame.height / 2.0
}
11
votes

create new interface in your .h file like

@interface CornerClip : UIImageView

@end

and implementation in .m file like

@implementation cornerClip


-(void)layoutSubviews
{
    [super layoutSubviews];

    CGFloat radius = self.bounds.size.width / 2.0;

    self.layer.cornerRadius = radius;

}

@end

now just give class as "CornerClip" to your imageview. 100% working... Enjoy

5
votes

First of all, I should mention that u can get a circle shape for your UIView/UIImageView only if the width and height will be equal. It's important to understand. In all other cases (when width != height), you won't get a circle shape because the initial shape of your UI instance was a rectangle.

OK, with this so UIKit SDK provides for developers a mechanism to manipulate the UIview's layer instance to change somehow any of layer's parameters, including setting up a mask to replace the initial shape of UIView element with the custom one. Those instruments are IBDesignable/IBInspectable. The goal is to preview our custom views directly through Interface Builder.

So using those keywords we can write our custom class, which will deal only with the single condition whether we need to round corners for our UI element or not.

For example, let's create the class extended from the UIImageView.

@IBDesignable
class UIRoundedImageView: UIImageView {

    @IBInspectable var isRoundedCorners: Bool = false {
        didSet { setNeedsLayout() }
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        if isRoundedCorners {
            let shapeLayer = CAShapeLayer()
            shapeLayer.path = UIBezierPath(ovalIn:
                CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: bounds.height
            )).cgPath
            layer.mask = shapeLayer
        }
        else {
            layer.mask = nil
        }

    }

}

After setting the class name for your UIImageView element (where the dog picture is), in your storyboard, you will get a new option, appeared in the Attributes Inspector menu (details at the screenshot).

The final result should be like this one.

4
votes

It seems when you add one view as a subview of another that netted view will not necessarily have the same height as its superview. That's what the problem seems like. The solution is to not add your imageView as a subview, but have it on top of your backgroundView. In the image below I'm using a UILabel as my backgroundView.

screenshot

Also in your case, when you're setting the cornerRadius use this: let radius: CGFloat = self.bounds.size.height / 2.0.

1
votes

With my hacky solution you'll get smooth corner radius animation alongside frame size change.

Let's say you have ViewSubclass : UIView. It should contain the following code:

class ViewSubclass: UIView {
    var animationDuration : TimeInterval?
    let imageView = UIImageView()
    //some imageView setup code

    override func layoutSubviews() {
        super.layoutSubviews()

        if let duration = animationDuration {
            let anim = CABasicAnimation(keyPath: "cornerRadius")
            anim.fromValue = self.imageView.cornerRadius
            let radius = self.imageView.frame.size.width / 2
            anim.toValue = radius
            anim.duration = duration
            self.imageView.layer.cornerRadius = radius
            self.imageView.layer.add(anim, forKey: "cornerRadius")
        } else {
            imageView.cornerRadius = imageView.frame.size.width / 2
        }

        animationDuration = nil
    }
}

An then you'll have to do this:

let duration = 0.4 //For example
instanceOfViewSubclass.animationDuration = duration
UIView.animate(withDuration: duration, animations: { 
    //Your animation
    instanceOfViewSubclass.layoutIfNeeded()
})

It's not beautiful, and might not work for complex multi-animations, but does answer the question.

1
votes

Swift 4+ clean solution based on omaralbeik's answer

import UIKit

extension UIImageView {

    func setRounded(borderWidth: CGFloat = 0.0, borderColor: UIColor = UIColor.clear) {
        layer.cornerRadius = frame.width / 2
        layer.masksToBounds = true
        layer.borderWidth = borderWidth
        layer.borderColor = borderColor.cgColor
    }
}

Sample usage in UIViewController

1.Simply rounded UIImageView

override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        imageView.setRounded()
    }

2.Rounded UIImageView with border width and color

override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        imageView.setRounded(borderWidth: 1.0, borderColor: UIColor.red)
    }
1
votes

write this code

override viewDidLayoutSubViews() {
profileImageView.layer.cornerRadius = profileImageView.frame.size.width / 2
profileImageView.clipsToBounds = true

}

in this case it will called after calculating the autolayout calculations in the first code you called the cornerradius code before calculating the actual size of the view cuz it's dynamically calculated using aspect ratio , the actual corner radius is calculated before equaling the width and the height of the view

0
votes

I have same problem, and Now I get what happened here, hope some ideas can help you: there are something different between the tows:

  1. your profileImageView in storyboard
  2. your profileImageView in viewDidLoad

the size of bound and frame is different when viewDidLoad and in storyboard,just because view is resize for different device size. You can try it print(profileImageView.bounds.size) in viewDidLoad and viewDidAppear you will find the size in viewDidLoad you set cornerRadius is not the real "running" size.

a tips for you: you can use a subClass of ImageView to avoid it, or do not use it in storyboard,

0
votes

If you have subclassed the UIImageView. Then just add this piece of magical code in it.

Written in : Swift 3

override func layoutSubviews() {
    super.layoutSubviews()
    if self.isCircular! {
        self.layer.cornerRadius = self.bounds.size.width * 0.50
    }
}
0
votes

I am quite new to iOS native development, but I had the same problem and found a solution.

this was the goal

So the green background has this constraints:

backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.topAnchor.constraint(equalTo: superview!.safeAreaLayoutGuide.topAnchor).isActive = true
backgroundView.leftAnchor.constraint(equalTo: superview!.leftAnchor).isActive = true
backgroundView.widthAnchor.constraint(equalTo: superview!.widthAnchor).isActive = true
backgroundView.heightAnchor.constraint(equalTo: superview!.heightAnchor, multiplier: 0.2).isActive = true

The image constraints:

avatar.translatesAutoresizingMaskIntoConstraints = false
avatar.heightAnchor.constraint(equalTo: backgroundView.heightAnchor, multiplier: 0.8).isActive = true
avatar.widthAnchor.constraint(equalTo: backgroundView.heightAnchor, multiplier: 0.8).isActive = true
avatar.centerYAnchor.constraint(equalTo: backgroundView.centerYAnchor, constant: 0).isActive = true
avatar.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: 20).isActive = true

on viewWillLayoutSubviews() method I set the corner radius to

avatar.layer.cornerRadius = (self.frame.height * 0.2 * 0.8) / 2

Basically, I am simply calculating the height of the image and then divide it by 2. 0.2 is the backgroungView height constraint multiplier and 0.8 the image width/height constraint multiplier.

0
votes

Solution: Crop the image

[imageView setImage:[[imageView image] imageWithRoundedCorners:imageView.image.size.width/2]];
imageView.contentMode = UIViewContentModeScaleAspectFill;

I was looking for the same solution for profile pictures. After some hit and try and going through available functions, I came across something which works and is a nice way to ensure its safe. You can use the following function to crop out a round image from the original image and then you need not worry about the corner radius.

Post this even if your view size changes the image remains round and looks good.

-1
votes

Add a 1:1 aspect ratio constraint to the imageView for it to remain circular, despite any height or width changes.

-1
votes

I added custom IBInspectable cornerRadiusPercent, so you can do it without any code.

class RoundButton: UIButton {

    override var bounds: CGRect {
        didSet {
            updateCornerRadius()
        }
    }


    //private var cornerRadiusWatcher : CornerRadiusPercent?
    @IBInspectable var cornerRadiusPercent: CGFloat = 0 {

        didSet {
            updateCornerRadius()
        }
    }

    func updateCornerRadius()
    {
        layer.cornerRadius = bounds.height * cornerRadiusPercent
    }

}
-5
votes

Can be easily done by creating an IBOutlet for the constraint which needs to be changed at runtime. Below is the code:

  1. Create a IBOutlet for the constraint that needs to be changed at run time.

    @IBOutlet var widthConstraint: NSLayoutConstraint!
    
  2. Add below code in viewDidLoad():

    self.widthConstraint.constant = imageWidthConstraintConstant()
    

Below function determines for device types change the width constraint accordingly.

func imageWidthConstraintConstant() -> CGFloat {

    switch(self.screenWidth()) {

    case 375:
        return 100

    case 414:
        return 120

    case 320:
        return 77

    default:
        return 77
    }
}