19

I'm drawing round avatar pics, by just applying cornerRadius to a UIImageView's layer, and also adding a border via borderWith and borderColor. Like so:

self.layer.masksToBounds = YES;
self.layer.cornerRadius = imageDimension / 2.f;
self.layer.borderWidth = 1.f;
self.layer.borderColor = borderColor.CGColor;

That works great, except for this tiny, but noticeable bleeding of content outside the border, like this:

Content bleeding outside the mask and border

Is there a way to just outset the border by a few 1/10 points, or inset the content more than the border?


Solution

Thanks to FelixLam, I came up with a nice solution and will leave it here for the afterworld:

@interface MMRoundImageViewWithBorder : UIView

- (id)initWithImage:(UIImage *)image borderWidth:(CGFloat)borderWidth;

@property (strong, nonatomic) UIImageView *imageView;
@property (assign, nonatomic) CGFloat borderWidth;
@property (strong, nonatomic) UIColor *borderColor;

@end

@implementation MMRoundImageViewWithBorder

- (id)initWithImage:(UIImage *)image borderWidth:(CGFloat)borderWidth {
    if (self = [super init]) {
        self.borderWidth = borderWidth;
        self.borderColor = UIColor.whiteColor;

        self.imageView = [[UIImageView alloc] initWithImage:image];
        [self addSubview:self.imageView];

        self.imageView.layer.masksToBounds = YES;
        self.layer.masksToBounds = YES;
    }
    return self;
}

- (void)setBorderColor:(UIColor *)borderColor {
    _borderColor = borderColor;
    self.backgroundColor = borderColor;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    [self refreshDimensions];
}

- (void)refreshDimensions {
    self.layer.cornerRadius = CGRectGetWidth(self.bounds) / 2.f;

    self.imageView.frame = CGRectInset(self.bounds, _borderWidth, _borderWidth);
    self.imageView.layer.cornerRadius = CGRectGetWidth(self.imageView.bounds) / 2.f;
}

- (void)setBorderWidth:(CGFloat)borderWidth {
    _borderWidth = borderWidth;
    [self refreshDimensions];
}

- (void)setFrame:(CGRect)frame {
    [super setFrame:frame];
    [self refreshDimensions];
}

@end
manmal
  • 3,900
  • 2
  • 30
  • 42
  • 1
    You can try to add a transparent 1 pixel border to your image file, so your image will be 2 pixels wider and higher, this should help with the blending – dariaa Oct 24 '13 at 12:21

7 Answers7

14

You could try to use a CAShapelayer with a circular path as the mask for the Layer instead of using the corner radius.

Alternatively you can place the image view in a container that has the border and is one/two pixel larger.

Felix Lamouroux
  • 7,414
  • 29
  • 46
1

As said in comments, you can add a transparent border to the image so it will be ok. Here is a piece of code to do so (a category to UIImageView) :

+ (UIImage *)imageWithImage:(UIImage *)image withTransparentBorderOfWidth:(CGFloat)width {
    CGSize size = image.size;
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(size.width + width * 2, size.height + width * 2), NO, 0.0);
    [image drawInRect:CGRectMake(width, width, size.width, size.height)];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
Tancrede Chazallet
  • 7,035
  • 6
  • 41
  • 62
  • 1
    This is a suboptimal solution. You will always have to regenerate the image if it is fetched from a cache or refreshed. Drawing a border around an image is IMO a rendering concern, not a content concern. – manmal Oct 24 '13 at 12:32
  • When there are 9 ways to a goal, don't choose a bad one. Or your codebase will rot very soon. – manmal Oct 24 '13 at 12:42
0

OP's solution is unnecessarily complicated. An easier and cleaner solution. Create a UIView, clip to bounds, mask, border, and corner radius like above and then add a UIIamgeview as a subview of that UIView. The UIIamgeview will be clipped by the parent view and you won't have the bleeding issue.

sudo
  • 1,648
  • 1
  • 20
  • 22
0

My UIButton needed a rounded corner radius which resulted in jagged edges. Setting borderStyle to .roundedRect fixed it.

button.borderStyle = .roundedRect
button.layer.cornerRadius = 4
button.layer.borderWidth = 1
button.layer.borderColor = .red.cgColor
Webdevotion
  • 1,223
  • 13
  • 24
0

Here's a subclass of UIImageView that solves this problem.

As the accepted answer suggests, it adds a rounded CAShapeLayer that has a border width and a corner radius 1px larger than "wanted". Its frame starts at CGPoint(x: 1, y: 1) and has a height 2px larger than the height of the image view. That way, it covers the bleed-through.

Swift 3+ solution

Subclass:

class BorderedRoundedImageView: UIImageView {
    let borderLayer = CALayer()
    
    var borderWidth: CGFloat!
    var borderColor: UIColor!
    
    func setUp() {
        borderLayer.borderWidth = borderWidth + 1
        borderLayer.borderColor = borderColor.cgColor
        layer.addSublayer(borderLayer)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        borderLayer.cornerRadius = layer.cornerRadius + 1
        borderLayer.frame = CGRect(x: -1, y: -1, width: frame.width + 2, height: frame.height + 2)
    }
}

Usage:

@IBOutlet weak var imageView: UIImageView!

[...]

imageView.layer.cornerRadius = 20
imageView.borderWidth = 2
imageView.borderColor = .lightGray
imageView.setUp()

Result:

screenshot of the result

Community
  • 1
  • 1
Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
-1

Try setting:

[self setClipToBounds:YES]
Javi Campaña
  • 1,481
  • 1
  • 22
  • 30
-1

I had the same problem and wanted a solution which would work well with a Storyboard. What I did was place my image inside of a view and then set both to a custom class which controls the behavior. I can now totally control the design from a Storyboard.

I've placed the code on GitHub.

https://github.com/brennanMKE/CircleButton

What it does is override the drawRect function in UIView for both a UIButton and regular UIView so it can work with many views. A wrapper view is also used to control the border color and width. I simply make sure there is some margin between the wrapped view and the superview and use the difference as the border width. The background color of the superview becomes the border color. Now I can make these changes quickly in many scenes in a Storyboard without custom coding each instance.

- (void)drawRect:(CGRect)rect {
    // round view and superview with a border using the background color of the superview
    self.layer.cornerRadius = CGRectGetWidth(self.frame) / 2;
    self.layer.masksToBounds = YES;

    if ([self.superview isKindOfClass:[SSTCircleWrapperView class]]) {
        self.superview.layer.cornerRadius = CGRectGetWidth(self.superview.frame) / 2;
        self.superview.layer.masksToBounds = YES;
        self.superview.layer.borderColor = self.superview.backgroundColor.CGColor;
        self.superview.layer.borderWidth = (CGRectGetWidth(self.superview.frame) - CGRectGetWidth(self.frame)) / 2;
    }
}
Brennan
  • 11,546
  • 16
  • 64
  • 86
  • Thanks. Does my solution not work in Storyboards (not that I really care)? Also set ```shouldRasterize``` to ```YES``` on both layers or performance will degrade considerably. Masking seems to be very expensive. – manmal Nov 13 '13 at 08:07
  • 1
    This is abusing draw rect, most of this stuff should only happen when the frame changes. – Colin Swelin Jun 10 '16 at 14:31