Drawing Code for iOS 7 Rounded Rectangles

Short story: We’ve decided to open source some Objective-C categories for drawing the new iOS 7 rounded rectangles. Code download below!

Long story: In iOS 7, Apple introduced a slightly different icon shape – it is no longer a simple rectangle with circular arcs in corners. The old design produced a subtle but noticeable “hump” in places where arc touches some of the straight lines. The new iOS 7 icon shape looks more “organic” and eliminates this effect.

Below is a comparison between iOS 6 and iOS 7 icon corners. Hover your mouse over the iBooks icon to see the difference more clearly:


iOS 6

iOS 7

difference

However, this change was not limited to app icons. All rounded rectangles drawn with UIKit now use the new, updated shape. This means that when you draw a rounded rectangle using UIBezierPath’s bezierPathWithRoundedRect:cornerRadius: method, you’ll get the new shape on iOS 7. Note that this method has been available since iOS 3.2 and it now works differently.

But… what if I want to draw the new rounded rectangle shape on iOS 6 or Mac?

First, we have to learn more about the new shape. How does it look like for different dimensions and corner radiuses? We’ve perfectly replicated the iOS 7 rounded rectangle behavior in JavaScript, so you can play with it below:

radius
width
height

Noticed something interesting? There are cases when a very small change of parameters produces a radically different shape. Curiously, even the number of bezier path elements (such as moveTo, lineTo, curveTo) varies. The first thing we’ve found out was that there are actually 3 kinds of shapes, depending on specific width, height and corner radius settings.

The differences between shape_1 and the old iOS 6 rounded rectangle shape can be easily overlooked. That is not the case with the other two. We are not sure if shape_2 and shape_3 exist on purpose (or if this is a bug in UIKit), but they sure can break your drawing code. Imagine you have an app that, for whatever reasons, draws a rounded rectangle and a line that touches it. You may be perfectly happy with how it looks on iOS 6. However, look what the same code draws on iOS 7:

To find out how exactly the new iOS 7 rounded rectangles are made, we needed to examine the generated bezier paths. If it were on Mac, we could simply access the elements of NSBezierPath using the elementAtIndex:associatedPoints: method, but the new shape is only available on iOS / UIKit.

Nevertheless, we can get the CGPath from an UIBezierPath, then use the CGPathApply(cgPath, object, applierFunction) Core Graphics function. CGPathApply calls applierFunction with each element of the CGPath as its argument, allowing us to collect the exact curve data.

Using measured values we can determine:

  1. the exact positions of all bezier control points for specific width/height/radius parameters
  2. when to draw shape_1, shape_2 or shape_3


We’ve found out that UIKit generates:

  • shape_1, when width > 3*radius AND height > 3*radius,
  • shape_2, when width > 3*radius XOR height > 3*radius,
  • shape_3, when width < 3*radius AND height < 3*radius,

where number 3 is only an approximated value (actually, it is around 3.0573299…)

We’ve also found out that for every bezier control point close to top left corner of the rectangle there exist constantX and constantY such that the position of the control point is always (constantX * cornerRadius, constantY * cornerRadius). Because the other 3 corners work similarly, all that remained was to find these control point position constants, which we did.

iOS 7 rounded rectangles are buggy.

Unfortunately, there are parameters for which the shapes generated by bezierPathWithRoundedRect:cornerRadius: are obviously wrong. The buggy behavior is accurately reproduced in the JavaScript example above (try parameters radius = 50, width = 150, height = 153). Or, run this code…

UIBezierPath* path = [UIBezierPath
    bezierPathWithRoundedRect: CGRectMake(0, 0, 150, 153)
                 cornerRadius: 50];
path.lineWidth = 1;
[UIColor.blackColor setStroke];
[path stroke];

…and you’ll get one of these:

We’ve reported this bug to Apple (rdar://15491334). Hopefully, it will be fixed in future versions of iOS.

Free code for everyone!

We’ve decided to open source some Objective-C categories for drawing the new iOS 7 rounded rectangles.

Because of the unusual behavior of shape_2, shape_3 and the weird drawing bug, we’ve created 2 categories. The first category always generates the well-behaving shape_1 and can be used in iOS 7 as well as in older versions of iOS. It’s short and simple:


#define TOP_LEFT(X, Y)\
    CGPointMake(rect.origin.x + X * limitedRadius,\
                rect.origin.y + Y * limitedRadius)
#define TOP_RIGHT(X, Y)\
    CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius,\
                rect.origin.y + Y * limitedRadius)
#define BOTTOM_RIGHT(X, Y)\
    CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius,\
                rect.origin.y + rect.size.height - Y * limitedRadius)
#define BOTTOM_LEFT(X, Y)\
    CGPointMake(rect.origin.x + X * limitedRadius,\
                rect.origin.y + rect.size.height - Y * limitedRadius)


+ (UIBezierPath*)bezierPathWithIOS7RoundedRect: (CGRect)rect 
                                  cornerRadius: (CGFloat)radius
{
    UIBezierPath* path = UIBezierPath.bezierPath;
    CGFloat limit = MIN(rect.size.width, rect.size.height) / 2 / 1.52866483;
    CGFloat limitedRadius = MIN(radius, limit);
    
    [path moveToPoint: TOP_LEFT(1.52866483, 0.00000000)];
    [path addLineToPoint: TOP_RIGHT(1.52866471, 0.00000000)];
    [path addCurveToPoint: TOP_RIGHT(0.66993427, 0.06549600)
            controlPoint1: TOP_RIGHT(1.08849323, 0.00000000) 
            controlPoint2: TOP_RIGHT(0.86840689, 0.00000000)];
    [path addLineToPoint: TOP_RIGHT(0.63149399, 0.07491100)];
    [path addCurveToPoint: TOP_RIGHT(0.07491176, 0.63149399) 
            controlPoint1: TOP_RIGHT(0.37282392, 0.16905899)  
            controlPoint2: TOP_RIGHT(0.16906013, 0.37282401)];
    [path addCurveToPoint: TOP_RIGHT(0.00000000, 1.52866483)  
            controlPoint1: TOP_RIGHT(0.00000000, 0.86840701)
            controlPoint2: TOP_RIGHT(0.00000000, 1.08849299)];
    [path addLineToPoint: BOTTOM_RIGHT(0.00000000, 1.52866471)];
    [path addCurveToPoint: BOTTOM_RIGHT(0.06549569, 0.66993493)
            controlPoint1: BOTTOM_RIGHT(0.00000000, 1.08849323)
            controlPoint2: BOTTOM_RIGHT(0.00000000, 0.86840689)];
    [path addLineToPoint: BOTTOM_RIGHT(0.07491111, 0.63149399)];
    [path addCurveToPoint: BOTTOM_RIGHT(0.63149399, 0.07491111)
            controlPoint1: BOTTOM_RIGHT(0.16905883, 0.37282392)
            controlPoint2: BOTTOM_RIGHT(0.37282392, 0.16905883)];
    [path addCurveToPoint: BOTTOM_RIGHT(1.52866471, 0.00000000)
            controlPoint1: BOTTOM_RIGHT(0.86840689, 0.00000000)
            controlPoint2: BOTTOM_RIGHT(1.08849323, 0.00000000)];
    [path addLineToPoint: BOTTOM_LEFT(1.52866483, 0.00000000)];
    [path addCurveToPoint: BOTTOM_LEFT(0.66993397, 0.06549569)
            controlPoint1: BOTTOM_LEFT(1.08849299, 0.00000000)
            controlPoint2: BOTTOM_LEFT(0.86840701, 0.00000000)];
    [path addLineToPoint: BOTTOM_LEFT(0.63149399, 0.07491111)];
    [path addCurveToPoint: BOTTOM_LEFT(0.07491100, 0.63149399)
            controlPoint1: BOTTOM_LEFT(0.37282401, 0.16905883)
            controlPoint2: BOTTOM_LEFT(0.16906001, 0.37282392)];
    [path addCurveToPoint: BOTTOM_LEFT(0.00000000, 1.52866471)
            controlPoint1: BOTTOM_LEFT(0.00000000, 0.86840689)
            controlPoint2: BOTTOM_LEFT(0.00000000, 1.08849323)];
    [path addLineToPoint: TOP_LEFT(0.00000000, 1.52866483)];
    [path addCurveToPoint: TOP_LEFT(0.06549600, 0.66993397)
            controlPoint1: TOP_LEFT(0.00000000, 1.08849299)
            controlPoint2: TOP_LEFT(0.00000000, 0.86840701)];
    [path addLineToPoint: TOP_LEFT(0.07491100, 0.63149399)];
    [path addCurveToPoint: TOP_LEFT(0.63149399, 0.07491100)
            controlPoint1: TOP_LEFT(0.16906001, 0.37282401)
            controlPoint2: TOP_LEFT(0.37282401, 0.16906001)];
    [path addCurveToPoint: TOP_LEFT(1.52866483, 0.00000000)
            controlPoint1: TOP_LEFT(0.86840701, 0.00000000)
            controlPoint2: TOP_LEFT(1.08849299, 0.00000000)];
    [path closePath];
    return path;
}

Download the simple, well-behaved category for drawing the new rounded rectangles:

Yet there are situations when you need the behavior to be as accurate as possible. PaintCode is a good example – we need to be able to draw exact replicas of iOS 7-like rounded rectangles on Mac. That’s why we’ve also created these fully compatible versions of the category, with iOS 7 UIKit bugs and everything:

Published on 18.11.2013 | View comments


COMMENTS


SUBSCRIBE TO OUR NEWSLETTER