6

I'm using this code to round off one corner of my UIView:

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
    self.view.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
    CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
self.view.layer.mask = maskLayer;
self.view.layer.masksToBounds = NO;

This code works, as long as I don't ever resize the view. If I make the view larger, the new area does not appear because it's outside the bounds of the mask layer (this mask layer does not automatically resize itself with the view). I could just make the mask as large as it will ever need to be, but it could be full-screen on the iPad so I'm worried about performance with a mask that big (I'll have more than one of these in my UI). Also, a super-sized mask wouldn't work for the situation where I need the upper right corner (alone) to be rounded off.

Is there a simpler, easier way to achieve this?

Update: here is what I'm trying to achieve: https://i.stack.imgur.com/dPBC9.png (the rounded corner I want is circled here in green).

I have achieved a working version of this, using a subclass of UINavigationController and overriding viewDidLayoutSubviews like so:

- (void)viewDidLayoutSubviews {
    CGRect rect = self.view.bounds;
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect 
        byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(8.0, 8.0)];
    self.maskLayer = [CAShapeLayer layer];
    self.maskLayer.frame = rect;
    self.maskLayer.path = maskPath.CGPath;
    self.view.layer.mask = self.maskLayer;
}

I then instantiate my UINavigationController subclass with my view controller, and then I offset the frame of the nav controller's view by 20px (y) to expose the status bar and leave a 44-px high navigation bar, as shown in the picture.

The code is working, except that it doesn't handle rotation very well at all. When the app rotates, viewDidLayoutSubviews gets called before the rotation and my code creates a mask that fits the view after rotation; this creates an undesirable blockiness to the rotation, where bits that should be hidden are exposed during the rotation. Also, whereas the app's rotation is perfectly smooth without this mask, with the mask being created the rotation becomes noticeably jerky and slow.

The iPad app Evomail also has rounded corners like this, and their app suffers from the same problem.

Rajesh Loganathan
  • 11,129
  • 4
  • 78
  • 90
MusiGenesis
  • 74,184
  • 40
  • 190
  • 334
  • 1
    can you post a pic of the existing solution , I'm having trouble visualising what you want – Warren Burton Jan 27 '14 at 17:02
  • I feel your pain...when designers want something that seems easy...How I might do it is to use cornerRadius and then hide the other 3 corners off screen. Always set the frame to be that much larger than the visible amount. If you need the entire view to be shown maybe try faking the other 3 corners with a view/views behind the view with rounded corners...Just throwing this out here – Jack Jan 27 '14 at 20:45
  • @JackWu: yeah, a solution that sort of worked was was to just create one mask at the beginning with the rounded upper-left corner and 1024 for the height and width. Problem is this is just an example - the designers actually want all four corners to be rounded. – MusiGenesis Jan 27 '14 at 21:25
  • @MusiGenesis See my solution to your problem. Works well. – Léo Natan Jan 27 '14 at 21:26
  • @MusiGenesis Wait...if the designers want all four corners to be rounded why don't you just use the cornerRadius property of the layer? Or do they want arbritrary corners to be rounded, one at a time? – Jack Jan 27 '14 at 21:32
  • Arbitrary corners - at most it would be three at once (upper left, upper right and lower left), sometimes only two or one. – MusiGenesis Jan 27 '14 at 22:07
  • I understand the background is an image. Is there ever animated content in the background? – TomSwift Jan 30 '14 at 20:14
  • @MusiGenesis Your bounty ends in 1 hour. Please accept an answer. – Léo Natan Feb 04 '14 at 14:51

9 Answers9

5

The problem is, CoreAnimation properties do not animate in UIKit animation blocks. You need to create a separate animation which will have the same curve and duration as the UIKit animation.

I created the mask layer in viewDidLoad. When the view is about to be layout, I only modify the path property of the mask layer.

You do not know the rotation duration inside the layout callback methods, but you do know it right before rotation (and before layout is triggered), so you can keep it there.

The following code works well.

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    //Keep duration for next layout.
    _duration = duration;
}

-(void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];

    UIBezierPath* maskPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(10, 10)];

    CABasicAnimation* animation;

    if(_duration > 0)
    {
        animation = [CABasicAnimation animationWithKeyPath:@"path"];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

        [animation setDuration:_duration];
        //Set old value
        [animation setFromValue:(id)((CAShapeLayer*)self.view.layer.mask).path];
        //Set new value
        [animation setToValue:(id)maskPath.CGPath];
    }
    ((CAShapeLayer*)self.view.layer.mask).path = maskPath.CGPath;

    if(_duration > 0)
    {
        [self.view.layer.mask addAnimation:animation forKey:@"path"];
    }

    //Zero duration for next layout.
    _duration = 0;
}
Léo Natan
  • 56,823
  • 9
  • 150
  • 195
2

I know this is a pretty hacky way of doing it but couldn't you just add a png over the top of the corner?

Ugly I know, but it won't affect performance, rotation will be fine if its a subview and users won't notice.

Martin
  • 1,135
  • 1
  • 8
  • 19
1

Two ideas:

  • Resize the mask when the view is resized. You don't get automatic resizing of sublayers the way you get automatic resizing of subviews, but you still get an event, so you can do manual resizing of sublayers.

  • Or... If this a view whose drawing and display you are in charge of, make the rounding of the corner a part of how you draw the view in the first place (by clipping). That is in fact the most efficient approach.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I don't think clipping will work here. An added complication is that this view is in a navigation controller with 44px of navigation bar at the top (like, it has 20px of black status bar and then 44px of translucent nav bar) with an x-coordinate of about 300. I do this by setting the frame of the nav controller's view to y=20 and it works fine, but the sharp corner it creates looks terrible. I don't think there's any way to use clipping to round the corner of the navigation controller's view. But I'd love to be proven wrong if it works! :) – MusiGenesis Jan 23 '14 at 03:22
  • 2
    BTW, I tried to convince the designer and my fellow programmers that this was going to be difficult to implement, but I was laughed at. – MusiGenesis Jan 23 '14 at 03:23
  • Ah, `UINavigationController` has a method `layoutSublayersForLayer:`. That seems like it might be the place to put this code. – MusiGenesis Jan 23 '14 at 03:40
1

You could subclass the view you are using and override "layoutSubviews"method. This one gets called everytime your view dimensions change.

Even if "self.view"(referenced in your code) is your viewcontroller's view, you can still set this view to a custom class in your storyboard. Here's the modified code for the subclass:

- (void)layoutSubviews {
    [super layoutSubviews];

    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
                          self.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
                          CGSizeMake(10.0, 10.0)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
    self.layer.masksToBounds = NO;
}
Thomas Keuleers
  • 6,075
  • 2
  • 32
  • 35
  • See my edit above. This is essentially what I'm doing, albeit by subclassing `UINavigationController` and overriding `viewDidLayoutSubviews`. The problem is that `layoutSubviews` and `viewDidLayoutSubviews` are called just once prior to animation or rotation (as opposed to continuously) creating an awkward, jerky effect. – MusiGenesis Jan 27 '14 at 19:24
  • To have them called continuously you need to override drawRect: as I've just suggested in my answer. – tanz Jan 29 '14 at 13:14
1

I think you should create a custom view that updates itself any time it is needed, which means anytime that setNeedsDisplay is called.

What I'm suggesting is to create a custom UIView subclass to be implemented as follows:

// OneRoundedCornerUIView.h

#import <UIKit/UIKit.h>

@interface OneRoundedCornerUIView : UIView //Subclass of UIView

@end


// OneRoundedCornerUIView.m

#import "OneRoundedCornerUIView.h"

@implementation OneRoundedCornerUIView

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

// Override drawRect as follows.
- (void)drawRect:(CGRect)rect
{
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
                          self.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
                          CGSizeMake(10.0, 10.0)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
    self.layer.masksToBounds = NO;
}
@end

Once you've done this you simply need to make your view an OneRoundedCornerUIView instance instead of an UIView one and your view will be updated smoothly every time you resize or change its frame. I've just done some testing and it seems to work perfectly.

This solution can also be easily customised in order to have a view for which you can easily set which corners should be on and which corners should not from your View Controller. Implementation as follows:

// OneRoundedCornerUIView.h

#import <UIKit/UIKit.h>

@interface OneRoundedCornerUIView : UIView //Subclass of UIView

// This properties are declared in the public API so that you can setup from your ViewController (it also works if you decide to add/remove corners at any time as the setter of each of these properties will call setNeedsDisplay - as shown in the implementation file)
@property (nonatomic, getter = isTopLeftCornerOn) BOOL topLeftCornerOn;
@property (nonatomic, getter = isTopRightCornerOn) BOOL topRightCornerOn;
@property (nonatomic, getter = isBottomLeftCornerOn) BOOL bottomLeftCornerOn;
@property (nonatomic, getter = isBottomRightCornerOn) BOOL bottomRightCornerOn;

@end


// OneRoundedCornerUIView.m

#import "OneRoundedCornerUIView.h"

@implementation OneRoundedCornerUIView

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

- (void) setTopLeftCornerOn:(BOOL)topLeftCornerOn
{
    _topLeftCornerOn = topLeftCornerOn;
    [self setNeedsDisplay];
}

- (void) setTopRightCornerOn:(BOOL)topRightCornerOn
{
    _topRightCornerOn = topRightCornerOn;
    [self setNeedsDisplay];
}

-(void) setBottomLeftCornerOn:(BOOL)bottomLeftCornerOn
{
    _bottomLeftCornerOn = bottomLeftCornerOn;
    [self setNeedsDisplay];
}

-(void) setBottomRightCornerOn:(BOOL)bottomRightCornerOn
{
    _bottomRightCornerOn = bottomRightCornerOn;
    [self setNeedsDisplay];
}

// Override drawRect as follows.
- (void)drawRect:(CGRect)rect
{
    UIRectCorner topLeftCorner = 0;
    UIRectCorner topRightCorner = 0;
    UIRectCorner bottomLeftCorner = 0;
    UIRectCorner bottomRightCorner = 0;

    if (self.isTopLeftCornerOn) topLeftCorner = UIRectCornerTopLeft;
    if (self.isTopRightCornerOn) topRightCorner = UIRectCornerTopRight;
    if (self.isBottomLeftCornerOn) bottomLeftCorner = UIRectCornerBottomLeft;
    if (self.isBottomRightCornerOn) bottomRightCorner = UIRectCornerBottomRight;

    UIRectCorner corners = topLeftCorner | topRightCorner | bottomLeftCorner | bottomRightCorner;

    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
                          self.bounds byRoundingCorners:(corners) cornerRadii:
                          CGSizeMake(10.0, 10.0)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
    self.layer.masksToBounds = NO;
}

@end
tanz
  • 2,557
  • 20
  • 31
  • Having such a complex `drawRect:` is never a good idea, especially when this can be accomplished differently. – Léo Natan Jan 30 '14 at 23:26
  • I agree that drawRect shouldn't be overriwretten when it's not strictly necessary - but I think this is one of that cases in which makes sense and it's the best approach. We want the view to draw itself with a rounded corner in any circumstances - that's what drawRect is there for. Why do we need to introduce complex workarounds to handle the animation when we can just ask the view to draw itself whenever that is required? I find this approach more linear and clean. Also there are only few lines of code in drawRect - you might need much more complex drawings than that in some situations. – tanz Jan 31 '14 at 00:39
  • Because `drawRect:` can be called in circumstances where you would not want to reapply the mask. Because during an animation, applying a new mask would hurt performance greatly. Because there are already provided and optimized APIs for animations. There is a reason why animations are not implemented inside the drawing code of views. – Léo Natan Jan 31 '14 at 00:47
1

I'm a fan of doing what @Martin suggests. As long as there isn't animated content behind the rounded-corner then you can pull this off - even with a bitmap image displayed behind the frontmost view needing the rounded corner.

I created a sample project to mimic your screenshot. The magic happens in a UIView subclass called TSRoundedCornerView. You can place this view anywhere you want - above the view you want to show a rounded corner on, set a property to say what corner to round (adjust the radius by adjusting the size of the view), and setting a property that is the "background view" that you want to be visible in the corner.

Here's the repo for the sample: https://github.com/TomSwift/testRoundedCorner

And here's the drawing magic for the TSRoundedCornerView. Basically we create an inverted clip path with our rounded corner, then draw the background.

- (void) drawRect:(CGRect)rect
{
    CGContextRef gc = UIGraphicsGetCurrentContext();

    CGContextSaveGState(gc);
    {
        // create an inverted clip path
        // (thanks rob mayoff: http://stackoverflow.com/questions/9042725/drawrect-how-do-i-do-an-inverted-clip)
        UIBezierPath* bp = [UIBezierPath bezierPathWithRoundedRect: self.bounds
                                                 byRoundingCorners: self.corner // e.g. UIRectCornerTopLeft
                                                       cornerRadii: self.bounds.size];
        CGContextAddPath(gc, bp.CGPath);
        CGContextAddRect(gc, CGRectInfinite);
        CGContextEOClip(gc);

        // self.backgroundView is the view we want to show peering out behind the rounded corner
        // this works well enough if there's only one layer to render and not a view hierarchy!
        [self.backgroundView.layer renderInContext: gc];

//$ the iOS7 way of rendering the contents of a view.  It works, but only if the UIImageView has already painted...  I think.
//$ if you try this, be sure to setNeedsDisplay on this view from your view controller's viewDidAppear: method.
//        CGRect r = self.backgroundView.bounds;
//        r.origin = [self.backgroundView convertPoint: CGPointZero toView: self];
//        [self.backgroundView drawViewHierarchyInRect: r
//                                  afterScreenUpdates: YES];
    }
    CGContextRestoreGState(gc);
}
TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • Interesting, I'll give this a try as well. The background is a gradient, but it's not animated in any way. – MusiGenesis Jan 30 '14 at 22:04
  • A further optimization might be to render the background+corner just once into a UIImage, and either draw that image from drawRect, or via a UIImageView. Depends on your needs/usage. – TomSwift Jan 30 '14 at 22:08
0

I thought about this again and I think there is a simpler solution. I updated my sample to showcase both solutions.

The new solution is to simply create a container view that has 4 rounded corners (via CALayer cornerRadius). You can size that view so only the corner you're interested in is visible on screen. This solution doesn't work well if you need 3 corners rounded, or two opposite (on the diagonal) corners rounded. I think it works in most other cases, including the one you've described in your question and screenshot.

Here's the repo for the sample: https://github.com/TomSwift/testRoundedCorner

TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • I thought about doing this as well, but it was too difficult to fold it into the existing UI structure without some major refactoring. I ended up just creating rounded-corner bitmaps and adding them to the navigation controller's view. – MusiGenesis Jul 24 '14 at 14:54
0

Try this. Hope this will helps you.

UIView* parent = [[UIView alloc] initWithFrame:CGRectMake(10,10,100,100)];
parent.clipsToBounds = YES;

UIView* child = [[UIView alloc] new];
child.clipsToBounds = YES;
child.layer.cornerRadius = 3.0f;
child.backgroundColor = [UIColor redColor];
child.frame = CGRectOffset(parent.bounds, +4, -4);


[parent addSubView:child];
Rajesh Loganathan
  • 11,129
  • 4
  • 78
  • 90
0

If you want to do it in Swift I could advice you to use an extension of an UIView. By doing so all subclasses will be able to use the following method:

import QuartzCore

extension UIView {
    func roundCorner(corners: UIRectCorner, radius: CGFloat) {
        let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSizeMake(radius, radius))
        var maskLayer = CAShapeLayer()
        maskLayer.frame = self.bounds;
        maskLayer.path = maskPath.CGPath;
        self.layer.mask = maskLayer;
    }
}

self.anImageView.roundCorner(UIRectCorner.TopRight, radius: 10)
Kevin Delord
  • 2,498
  • 24
  • 22
  • Yeah, the problem here is the use of a masking path for the corners, regardless of whether you're using Swift or not. Doing this generally works in a static situation, but it makes the rotation animation unacceptably jerky and non-smooth. – MusiGenesis Jul 24 '14 at 14:51
  • You are right about rotation animations. I'm afraid you/we will have to go for a more complicated solution then – Kevin Delord Jul 25 '14 at 11:32