Rounded UIView using CALayers - only some corners - How?

66,533

Solution 1

I used the answer over at How do I create a round cornered UILabel on the iPhone? and the code from How is a rounded rect view with transparency done on iphone? to make this code.

Then I realized I'd answered the wrong question (gave a rounded UILabel instead of UIImage) so I used this code to change it:

http://discussions.apple.com/thread.jspa?threadID=1683876

Make an iPhone project with the View template. In the view controller, add this:

- (void)viewDidLoad
{
    CGRect rect = CGRectMake(10, 10, 200, 100);
    MyView *myView = [[MyView alloc] initWithFrame:rect];
    [self.view addSubview:myView];
    [super viewDidLoad];
}

MyView is just a UIImageView subclass:

@interface MyView : UIImageView
{
}

I'd never used graphics contexts before, but I managed to hobble together this code. It's missing the code for two of the corners. If you read the code, you can see how I implemented this (by deleting some of the CGContextAddArc calls, and deleting some of the radius values in the code. The code for all corners is there, so use that as a starting point and delete the parts that create corners you don't need. Note that you can make rectangles with 2 or 3 rounded corners too if you want.

The code's not perfect, but I'm sure you can tidy it up a little bit.

static void addRoundedRectToPath(CGContextRef context, CGRect rect, float radius, int roundedCornerPosition)
{

    // all corners rounded
    //  CGContextMoveToPoint(context, rect.origin.x, rect.origin.y + radius);
    //  CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y + rect.size.height - radius);
    //  CGContextAddArc(context, rect.origin.x + radius, rect.origin.y + rect.size.height - radius, 
    //                  radius, M_PI / 4, M_PI / 2, 1);
    //  CGContextAddLineToPoint(context, rect.origin.x + rect.size.width - radius, 
    //                          rect.origin.y + rect.size.height);
    //  CGContextAddArc(context, rect.origin.x + rect.size.width - radius, 
    //                  rect.origin.y + rect.size.height - radius, radius, M_PI / 2, 0.0f, 1);
    //  CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, rect.origin.y + radius);
    //  CGContextAddArc(context, rect.origin.x + rect.size.width - radius, rect.origin.y + radius, 
    //                  radius, 0.0f, -M_PI / 2, 1);
    //  CGContextAddLineToPoint(context, rect.origin.x + radius, rect.origin.y);
    //  CGContextAddArc(context, rect.origin.x + radius, rect.origin.y + radius, radius, 
    //                  -M_PI / 2, M_PI, 1);

    // top left
    if (roundedCornerPosition == 1) {
        CGContextMoveToPoint(context, rect.origin.x, rect.origin.y + radius);
        CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y + rect.size.height - radius);
        CGContextAddArc(context, rect.origin.x + radius, rect.origin.y + rect.size.height - radius, 
                        radius, M_PI / 4, M_PI / 2, 1);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, 
                                rect.origin.y + rect.size.height);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, rect.origin.y);
        CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y);
    }   

    // bottom left
    if (roundedCornerPosition == 2) {
        CGContextMoveToPoint(context, rect.origin.x, rect.origin.y);
        CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y + rect.size.height);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, 
                                rect.origin.y + rect.size.height);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, rect.origin.y);
        CGContextAddLineToPoint(context, rect.origin.x + radius, rect.origin.y);
        CGContextAddArc(context, rect.origin.x + radius, rect.origin.y + radius, radius, 
                        -M_PI / 2, M_PI, 1);
    }

    // add the other corners here


    CGContextClosePath(context);
    CGContextRestoreGState(context);
}


-(UIImage *)setImage
{
    UIImage *img = [UIImage imageNamed:@"my_image.png"];
    int w = img.size.width;
    int h = img.size.height;

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, 4 * w, colorSpace, kCGImageAlphaPremultipliedFirst);

    CGContextBeginPath(context);
    CGRect rect = CGRectMake(0, 0, w, h);


    addRoundedRectToPath(context, rect, 50, 1);
    CGContextClosePath(context);
    CGContextClip(context);

    CGContextDrawImage(context, rect, img.CGImage);

    CGImageRef imageMasked = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    [img release];

    return [UIImage imageWithCGImage:imageMasked];
}

alt text http://nevan.net/skitch/skitched-20100224-092237.png

Don't forget that you'll need to get the QuartzCore framework in there for this to work.

Solution 2

Starting in iOS 3.2, you can use the functionality of UIBezierPaths to create an out-of-the-box rounded rect (where only corners you specify are rounded). You can then use this as the path of a CAShapeLayer, and use this as a mask for your view's layer:

// Create the path (with only the top-left corner rounded)
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds 
                                               byRoundingCorners:UIRectCornerTopLeft
                                                     cornerRadii:CGSizeMake(10.0, 10.0)];

// Create the shape layer and set its path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = imageView.bounds;
maskLayer.path = maskPath.CGPath;

// Set the newly created shape layer as the mask for the image view's layer
imageView.layer.mask = maskLayer;

And that's it - no messing around manually defining shapes in Core Graphics, no creating masking images in Photoshop. The layer doesn't even need invalidating. Applying the rounded corner or changing to a new corner is as simple as defining a new UIBezierPath and using its CGPath as the mask layer's path. The corners parameter of the bezierPathWithRoundedRect:byRoundingCorners:cornerRadii: method is a bitmask, and so multiple corners can be rounded by ORing them together.


EDIT: Adding a shadow

If you're looking to add a shadow to this, a little more work is required.

Because "imageView.layer.mask = maskLayer" applies a mask, a shadow will not ordinarily show outside of it. The trick is to use a transparent view, and then add two sublayers (CALayers) to the view's layer: shadowLayer and roundedLayer. Both need to make use of the UIBezierPath. The image is added as the content of roundedLayer.

// Create a transparent view
UIView *theView = [[UIView alloc] initWithFrame:theFrame];
[theView setBackgroundColor:[UIColor clearColor]];

// Create the path (with only the top-left corner rounded)
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:theView.bounds 
                                               byRoundingCorners:UIRectCornerTopLeft
                                                     cornerRadii:CGSizeMake(10.0f, 10.0f)];

// Create the shadow layer
CAShapeLayer *shadowLayer = [CAShapeLayer layer];
[shadowLayer setFrame:theView.bounds];
[shadowLayer setMasksToBounds:NO];
[shadowLayer setShadowPath:maskPath.CGPath];
// ...
// Set the shadowColor, shadowOffset, shadowOpacity & shadowRadius as required
// ...

// Create the rounded layer, and mask it using the rounded mask layer
CALayer *roundedLayer = [CALayer layer];
[roundedLayer setFrame:theView.bounds];
[roundedLayer setContents:(id)theImage.CGImage];

CAShapeLayer *maskLayer = [CAShapeLayer layer];
[maskLayer setFrame:theView.bounds];
[maskLayer setPath:maskPath.CGPath];

roundedLayer.mask = maskLayer;

// Add these two layers as sublayers to the view
[theView.layer addSublayer:shadowLayer];
[theView.layer addSublayer:roundedLayer];

Solution 3

I have used this code in many places in my code and it works 100% correctly. You can change any corder by changed one property "byRoundingCorners:UIRectCornerBottomLeft"

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerBottomLeft cornerRadii:CGSizeMake(10.0, 10.0)];

                CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
                maskLayer.frame = view.bounds;
                maskLayer.path = maskPath.CGPath;
                view.layer.mask = maskLayer;
                [maskLayer release];

Solution 4

In iOS 11, we can now round some corners only

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 8
view.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]

Solution 5

CALayer extension with Swift 3+ syntax

extension CALayer {

    func round(roundedRect rect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadii: CGSize) -> Void {
        let bp = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: cornerRadii)
        let sl = CAShapeLayer()
        sl.frame = self.bounds
        sl.path = bp.cgPath
        self.mask = sl
    }
}

It can be used like:

let layer: CALayer = yourView.layer
layer.round(roundedRect: yourView.bounds, byRoundingCorners: [.bottomLeft, .topLeft], cornerRadii: CGSize(width: 5, height: 5))
Share:
66,533
Sagar Kothari
Author by

Sagar Kothari

iOS App Developer, Android App Developer, ReactJS Developer, Flutter

Updated on October 17, 2020

Comments

  • Sagar Kothari
    Sagar Kothari over 3 years

    In my application - there are four buttons named as follows:

    • Top - left
    • Bottom - left
    • Top - right
    • Bottom - right

    Above the buttons there is an image view (or a UIView).

    Now, suppose a user taps on - top - left button. Above image / view should be rounded at that particular corner.

    I am having some difficulty in applying rounded corners to the UIView.

    Right now I am using the following code to apply the rounded corners to each view:

        // imgVUserImg is a image view on IB.
        imgVUserImg.image=[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"any Url Here"];
        CALayer *l = [imgVUserImg layer];
        [l setMasksToBounds:YES];
        [l setCornerRadius:5.0];  
        [l setBorderWidth:2.0];
        [l setBorderColor:[[UIColor darkGrayColor] CGColor]];
    

    Above code is applying the roundness to each of corners of supplied View. Instead I just wanted to apply roundness to selected corners like - top / top+left / bottom+right etc.

    Is it possible? How?

  • Luke47
    Luke47 over 12 years
    Nice one, this helped me a lot on grouped UITableView cell's selectedBackgroundViews :-)
  • Chris Wagner
    Chris Wagner over 12 years
    Anyway to add a drop shadow to this view after you've rounded the corners? Attempted the following without success. ` self.layer.masksToBounds = NO; self.layer.shadowColor = [UIColor blackColor].CGColor; self.layer.shadowRadius = 3; self.layer.shadowOffset = CGSizeMake(-3, 3); self.layer.shadowOpacity = 0.8; self.layer.shouldRasterize = YES; `
  • Stuart
    Stuart over 12 years
    @ChrisWagner: See my edit, with regards to applying a shadow to the rounded view.
  • Chris Wagner
    Chris Wagner over 12 years
    @StuDev excellent! I had gone with a subview for the shadow view but this is much nicer! Didn't think to add sub layers like this. Thanks!
  • Fernando Redondo
    Fernando Redondo over 12 years
    Why does my image becomes white, then when I scroll the UITableView and the cell is recreated - it works! Why?
  • Steven Kramer
    Steven Kramer over 12 years
    Works as advertised in 5 minutes after fruitlessly trying to generate bitmaps from vector paths to use as layer masks :)
  • spiralstairs
    spiralstairs over 12 years
    In case anyone is trying to round two corners in one mask, like me, use the OR operator like this: byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight
  • Stuart
    Stuart over 12 years
    CALayer's mask property is not directly animatable, however you can easily animate the maskLayer's path property. A CABasicAnimation should work fine. The compiler will show a warning for the fromValue and toValue - suppress it by casting the CGPath to id.
  • Mark Struzinski
    Mark Struzinski about 12 years
    This was an excellent answer. Thank you for taking the time to be so thorough
  • Cocoadelica
    Cocoadelica about 11 years
    Another upvote, just added mine to say that as of March 2013 this works in iOS 6.1.2
  • Desert Rose
    Desert Rose about 11 years
    IF anybody is confused about ORing you can use following code. UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_tableView.bounds byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight cornerRadii:CGSizeMake(10.0, 10.0)]; CAShapeLayer *maskLayer = [CAShapeLayer layer]; maskLayer.frame = _tableView.bounds; maskLayer.path = maskPath.CGPath; _tableView.layer.mask = maskLayer;
  • Bushra Shahid
    Bushra Shahid almost 11 years
    @Luke47 can you please share how because ive been trying without success. Even after Oring my cells' right corners both top and bottom dont get rounded...plus the greyish boundary is also covered
  • MusiGenesis
    MusiGenesis over 10 years
    @Stuart: is there any way to make this mask auto-resize itself? I'm trying to use this code to round off only the upper-left corner of a view. The code works, but only with whatever size the view is when I call this code. If I then make my view larger I can't see it, because the new area is outside of the mask.
  • Stuart
    Stuart over 10 years
    @MusiGenesis UIViews autoresizes subviews according to their autoresizingMask, however sublayers that don't back such a UIView aren't autoresized, so you'll have to do it manually. It's easy though - whenever you resize your view (or in layoutSubviews if your view is subclassed, or even in your view controller's viewWill/DidLayoutSubviews method if appropriate) get a reference to the mask (CAShapeLayer *mask = (CAShapeLayer *)view.layer.mask;), create a new UIBezierPath using the view's new bounds & change the path (mask.path = newPath.CGPath; mask.frame = view.bounds;).
  • anneblue
    anneblue about 10 years
    When using this solution on UITableView-subviews (e.g. UITableViewCells or UITableViewHeaderFooterViews) it results in bad smoothness when scrolling. Another approach for that use is this solution with a better performance (it adds a cornerRadius to all corners). To 'show' only specific corners rounded (like top and bottom right corners) I added a subview with a negative value on its frame.origin.x and assigned the cornerRadius to its layer. If someone found a better solution, I'm interested.
  • anneblue
    anneblue about 10 years
    When using this solution on UITableView-subviews (e.g. UITableViewCells or UITableViewHeaderFooterViews) it results in bad smoothness when scrolling. Another approach for that use is this solution with a better performance (it adds a cornerRadius to all corners). To 'show' only specific corners rounded (like top and bottom right corners) I added a subview with a negative value on its frame.origin.x and assigned the cornerRadius to its layer. If someone found a better solution, I'm interested.
  • user3099609
    user3099609 over 9 years
    Doesn't work as expected with views whose size might change. Example: a UILabel. Setting the mask path and then changing the size by setting text to it will keep the old mask. Will need subclassing and overloading the drawRect.
  • Stuart
    Stuart about 8 years
    This isn't the right way to solve the problem. You should only override drawRect(_:) if you're doing custom drawing in your view (e.g. with Core Graphics), otherwise even an empty implementation can impact performance. Creating a new mask layer every time is unnecessary too. All you actually need to do is update the mask layer's path property when the view's bounds change, and the place to do that is within an override of the layoutSubviews() method.
  • HuaTham
    HuaTham over 7 years
    The doc doesn't explicitly say this, but when you set layer.mask, this layer becomes the mask's parent. That is why when we set mask.frame for rounded corners we set it to the layer.bounds: mask will cover the entire area of the parent layer, in parent's own coordinate.
  • xdeleon
    xdeleon over 2 years
    Just an additional note that this worked with Storyboard using autolayout. Some of the other solutions posted didn't work with autolayout.