iOS: frame.size.width/2 doesn't produce a circle on every device
Asked Answered
S

6

15

I'm aware that the formulae frame.size.width/2 should produce a circle border, however in XCode I am currently experiencing some discrepancies.

I have two test devices (iPhone6 and 5th gen iPod touch) I also have the simulator running. Both my devices display correctly but the simulator draws my circle as a rounded rectangle:

enter image description here

The code I am using to achieve this (although very simple) is:

imgAvatar.layer.masksToBounds = true
imgAvatar.clipsToBounds = true
imgAvatar.layer.cornerRadius = imgAvatar.frame.size.width/2
imgAvatar.layer.borderWidth = 5
imgAvatar.layer.borderColor = UIColor.whiteColor().CGColor

Is there any reason why this is happening? It's driving me insane!

UPDATE To clear confusion, the UIImageView is declared in my storyboard as 190x190 it also has a 1:1 aspect ratio constraint applied to it to ensure it maintains a proportional width and height.

UPDATE 2 To put any suspicions regarding my auto-layout constraints to bed, I have attached the below image which shows the constraints set for imgAvatar. As you can see a the width and height match and the AR is set to ensure it maintains that 1:1 ratio. I hope that clears up any further doubts

enter image description here

ANSWER Leo pointed out an extremely practical and reusable solution to fix this problem, using Swift extensions one can ensure that a given UIImage is always square, thus always generating a circle, I have posted Leo's solution below:

extension UIImage {
    var circleMask: UIImage? {
        let square = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
        let imageView = UIImageView(frame: .init(origin: .init(x: 0, y: 0), size: square))
        imageView.contentMode = .scaleAspectFill
        imageView.image = self
        imageView.layer.cornerRadius = square.width/2
        imageView.layer.borderColor = UIColor.white.cgColor
        imageView.layer.borderWidth = 5
        imageView.layer.masksToBounds = true
        UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
        defer { UIGraphicsEndImageContext() }
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        imageView.layer.render(in: context)
        return UIGraphicsGetImageFromCurrentImageContext()
    }
}

imgAvatar.image = yourImage.circleMask
Schoolboy answered 16/4, 2015 at 20:11 Comment(11)
Can you try logging the imgFrame, as well as the resulting cornerRadius property? (Note that NSStringFromCGRect() might be helpful here.)Chiller
Where is this code (in what method)?Darby
It's in a method called initFrames which is called in viewDidLoadSchoolboy
That's the problem. viewDidLoad is too soon. The view controller's view isn't sized yet. Try viewWillAppear:.Darby
Moved to viewWillAppear seeing the same resultsSchoolboy
Do you check if your frame is square (4 equals sides) ?Campion
Do you try comment masksToBounds and clipsToBounds?Campion
Is the shape incorrect on the other simulators, or only on the iPhone 6 one?Mensch
Only in the simulators, not on either iPhone or iPodSchoolboy
It whould be in viewDidLayoutSubviews.Castro
Can you move your code to viewDidLayoutSubviews and share the result?Adiel
I
17

You can create an extension to apply the mask and border straight to your image so it will always work disregarding the screen size / scale:

edit/update:

Xcode 11 • Swift 5 or later

extension UIImage {
    var circleMask: UIImage? {
        let square = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
        let imageView = UIImageView(frame: .init(origin: .init(x: 0, y: 0), size: square))
        imageView.contentMode = .scaleAspectFill
        imageView.image = self
        imageView.layer.cornerRadius = square.width/2
        imageView.layer.borderColor = UIColor.white.cgColor
        imageView.layer.borderWidth = 5
        imageView.layer.masksToBounds = true
        UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
        defer { UIGraphicsEndImageContext() }
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        imageView.layer.render(in: context)
        return UIGraphicsGetImageFromCurrentImageContext()
    }
}

imgAvatar.image = yourImage.circleMask
Incapacitate answered 16/4, 2015 at 20:42 Comment(3)
Wow I love this solution, my first exposure to extensions and this is perfect. Thank you so much Leonardo!Schoolboy
Yes that is what i meant by subclassing :). Anyway good answer!Adiel
I am using a collection view with nearly 30 items. My collection view performs laggy with this solution. can you think of a more efficient solution in this case?Firman
A
8

Well, Autolayout should be the issue. As it can be seen the height of the imgAvatar on right is greater than the height of the imgAvatar on left.

The size of imgAvatar on right is 190 x 190 and the one on left is 200 x 200, but on the left the corner radius which you are setting is 190/2 i.e. 95px whereas it should be 100px.

Kindly set Height & Width Constraint in your nib file for imgAvatar, if you do not want the imgAvatar to resize by 1:1.

  1. Select imgAvatar in your nib.
  2. Editor > Pin > Height
  3. Select imgAvatar in your nib.
  4. Editor > Pin > Width

OR

Try moving your code to viewDidLayoutSubviews

OR

Subclass your UIImageView and set its corner radius in its awakeFromNib method

Kindly let us know if it worked. Thanks!

Adiel answered 16/4, 2015 at 21:12 Comment(4)
Yes you had already mentioned about your 1:1 ratio. So either keep it fixed and dont resize, or move your code to viewDidLayoutSubviews. Because what happens is you set the corner radius as 95px but autolayout resizes the image according to 1:1 ratio and you are setting it before the view has been resized.Adiel
I see, that makes total sense - this solution also worked for me when moving code to viewDidLayoutSubviews. - Upvoted, but accepting Leonardos answer as its nice for a failsafe solutionSchoolboy
I like your answer because you explained why the issue was occurring.Chiller
viewDidLayoutSubviews was the answer. Thanks.Revenuer
A
3

If you test in iOS8.3, you should call layoutIfNeed on your UI object before get its frame. Please read iOS8.3 Release notes

For example:

UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
// code that sets up the button, but doesn’t yet add it to a window
CGRect titleFrame = button.titleLabel.frame;
// code that relies on the correct value for titleFrame

You now need:

UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
// code that sets up the button, but doesn’t yet add it to a window
[button layoutIfNeeded]; // This is also safe pre-iOS 8.3
CGRect titleFrame = button.titleLabel.frame;
// code that relies on the correct value for titleFrame

It's said that for UIButton and sub-classes but you can try it also with your UI object.

When linking against iOS 8.3, any code that relies on layout information (such as the frame) of a UIButton subview when the button is not in the window hierarchy will need to send layoutIfNeeded to the button before retrieving layout information (such as button.titleLabel.frame) to ensure that the layout values are up to date.

Also, as the imgAvatar's cornerRadius was set to 1/2 of imgFrame's size, you will get a circle only if that cornerRadius value = 1/2 of imgAvatar's width (and height).

So, after the call:

imgAvatar.layer.cornerRadius = imgFrame.frame.size.width/2

verify :

  • frame size of imgAvatar and its cornerRadius
  • be sure that image is squared
Annabelleannabergite answered 16/4, 2015 at 20:23 Comment(4)
Even calling layoutIfNeeded() does not remedy this problem...very strange - thankyou for the suggestion though! :)Schoolboy
what is the value of imgFrame's frame? It seems that your imgAvatar is bigger than imgView, that why when you use 1/2 size of your imgView, the imgAvatar with that value of corner radius does not produce a circle.Annabelleannabergite
its (origin = (x = 205, y = 106), size = (width = 190, height = 190)) Schoolboy
It's the same size, I checked in the storyboard too. If it wasn't the same size then it wouldn't produce a circle on one environment and a rectangle in another?Schoolboy
F
2

The problem is that when the circle is applied, the bounds of the view are still considered the default (320x480) and so if you have set constraints related to the screen, the image will be bigger on iPhone 6/6+ for instance.

There are various solution, but if you want a fast one, just put your code:

imgAvatar.layer.cornerRadius = imgFrame.frame.size.width/2

in - viewDidLayoutSubviews in your view controller or in - layoutSubviews in your view, checking before if the cornerRadius is different from the width/2 of the image's frame. Something like this:

if (imgAvatar.layer.cornerRadius != imgFrame.frame.size.width/2) {
    imgAvatar.layer.cornerRadius = imgFrame.frame.size.width/2
}
Frentz answered 16/4, 2015 at 21:56 Comment(0)
S
0

The formula you used will only work for squared image. Are you using auto lay out? Maybe it could be stretched for the device that you used. If this is the case, then height will be higher than width,or vice versa. That is, the image will be some kind of rectangle, and it will not produce perfect circle.

Sixtasixteen answered 16/4, 2015 at 20:32 Comment(1)
I am applying the formula to the UIImageView and not the UIImage, the UIImageView is a 190x190 square, I applied 1:1 Aspect ratio constraints and width/height=190.Schoolboy
C
0

Thanks to @leo Dabus for the original solution Following is just an update for swift 4.2

import UIKit
extension UIImage {
    var circleMask: UIImage {
        let square = size.width < size.height ? CGSize(width: size.width, height: size.width) : CGSize(width: size.height, height: size.height)
        let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
        imageView.contentMode = UIView.ContentMode.scaleAspectFill
        imageView.image = self
        imageView.layer.cornerRadius = square.width/2
        imageView.layer.borderColor = UIColor.white.cgColor
        imageView.layer.borderWidth = 5
        imageView.layer.masksToBounds = true
        UIGraphicsBeginImageContext(imageView.bounds.size)
        imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
        let result = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return result!
    }
}
Corry answered 11/1, 2019 at 15:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.