Posts Tagged ‘WWDC’

Exploring iOS 7 Rounded Corners

Thursday, June 20th, 2013

Apple announced iOS 7 at the WWDC 2013. Compared to the previous iOS versions it gets a whole new look for the first time. There’s a lot of discussion going on whether it’s good or bad design. I personally like it very much, and it reminds me a bit of the huge step from Mac OS 9 to Mac OS X years ago. Many things have changed in iOS 7, but one little detail attracted my attention and that’s what this article is about: rounded corners. Apple is very obsessed about every little detail; in my opinion that’s one of the secrets why they make so beautiful and successful products. Usually a rounded corner is just a quarter of a circle, one might think. But Apple changed the form factor of rounded corners in iOS 7. Here is a little comparison:

Rounded Corners in iOS 6Rounded Corners in iOS 7


Animated iOS 6 vs 7 Comparison


To me, the corners in iOS 7 look much smoother and organic. These new corners are everywhere in iOS 7. One reason for this is that as a developer you get these new corners for free. Whenever you use the existing functions of the operating system to create rounded corners, you get the improved ones in iOS 7, e. g. when calling [UIBezierPath bezierPathWithRoundedRect:cornerRadius:].

Here is an image that shows the difference between the old and the new corners:

Corner Difference between iOS 6 and iOS 7


You see that not many pixels have changed, even at this big size, but nevertheless it has some subtle but perceptible impact in how the corners look and feel.

Currently there are a few discussions on how Apple draws these new corners; even some mathematical equations are published. I took a rather pragmatic approach to analyze it: I created a small test app that lets the system create a rounded rectangle and then introspects and visualizes the elements of the returned bezier path. Then I launched this app on iOS 6 and iOS 7. Here are the results:

Rounded Corner Bezier Path on iOS 6
Rounded Corner Bezier Path on iOS 7
A red cross marks every point where a straight line is drawn to. An orange cross with two green control points in between define a bezier curve. As you can see there are four little points where the path is not perfect yet: there are four (very) short straight lines from a curve end point (orange) to a new point (red) very closely together. These unnecessary four short lines can be stripped out of the path; maybe this will be fixed in the final version of iOS 7.

What’s also interesting is the fact that the edges of the rectangle are bent much earlier than before. In the picture below the blue area is where the lines are curved in iOS 6. The red area is where the lines are curved in iOS 7. It’s more than half the corner radius earlier:

Line bending areas

Update: Together with @flarup and @marcedwards we tried to figure out the exact radius for the corners being used for the app icons in iOS 7. Additionally I filtered out the annoying short lines metioned above and exported the UIBezierPath as SVG file. As far as we can see a radius of 27 pixels at 120×120 pixel icon size produces a pretty good match. Download it here.

In case you wonder how I introspected the UIBezierPath to draw the images above – here is the UIView code:

@interface CornerTestView ()
@property (nonatomic) CGPoint previousPoint;
@end

@implementation CornerTestView

static void pathInspector(void *info, const CGPathElement *element)
{
	CornerTestView *view = (__bridge CornerTestView *)info;
	[view addElement:element];
}

- (void)drawRect:(CGRect)dirtyRect
{
	[[UIColor whiteColor] setFill];
	UIRectFill(dirtyRect);

	CGRect rect = self.bounds;
	CGFloat size = MIN(CGRectGetWidth(rect), CGRectGetHeight(rect)) - 20;
	size = round(size / 2) * 2;
	rect = CGRectMake(round(CGRectGetMidX(rect) - size / 2), round(CGRectGetMidY(rect) - size / 2), size, size);
	CGFloat radius = size / 4;
	UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
	[[UIColor colorWithRed:0.90 green:0.93 blue:1.0 alpha:1.0] setFill];
	[path fill];

	CGPathRef cgPath = path.CGPath;
	CGPathApply(cgPath, (__bridge void *)(self), pathInspector);
}

static UIBezierPath *crossAtPoint(CGPoint p)
{
	UIBezierPath *cross = [UIBezierPath bezierPath];
	cross.lineWidth = 0.5;
	[cross moveToPoint:CGPointMake(p.x - 2, p.y - 2)];
	[cross addLineToPoint:CGPointMake(p.x + 2, p.y + 2)];
	[cross moveToPoint:CGPointMake(p.x - 2, p.y + 2)];
	[cross addLineToPoint:CGPointMake(p.x + 2, p.y - 2)];
	return cross;
}

- (void)addElement:(const CGPathElement *)element
{
	switch (element->type) {
		case kCGPathElementMoveToPoint:
			[[UIColor grayColor] setStroke];
			[crossAtPoint(element->points[0]) stroke];
			self.previousPoint = element->points[0];
			break;
		case kCGPathElementAddLineToPoint:
		{
			UIBezierPath *line = [UIBezierPath bezierPath];
			line.lineWidth = 0.25;
			[[UIColor lightGrayColor] setStroke];
			[line moveToPoint:self.previousPoint];
			[line addLineToPoint:element->points[0]];
			[line stroke];

			[[UIColor redColor] setStroke];
			[crossAtPoint(element->points[0]) stroke];
			self.previousPoint = element->points[0];
		}
			break;
		case kCGPathElementAddQuadCurveToPoint:
		{
			UIBezierPath *line = [UIBezierPath bezierPath];
			line.lineWidth = 0.25;
			[[UIColor lightGrayColor] setStroke];
			[line moveToPoint:self.previousPoint];
			[line addLineToPoint:element->points[0]];
			[line addLineToPoint:element->points[1]];
			[line stroke];
			[[UIColor cyanColor] setStroke];
			[crossAtPoint(element->points[0]) stroke];
			[[UIColor magentaColor] setStroke];
			[crossAtPoint(element->points[1]) stroke];
			self.previousPoint = element->points[1];
		}
			break;
		case kCGPathElementAddCurveToPoint:
		{
			UIBezierPath *line = [UIBezierPath bezierPath];
			line.lineWidth = 0.25;
			[[UIColor lightGrayColor] setStroke];
			[line moveToPoint:self.previousPoint];
			[line addLineToPoint:element->points[0]];
			[line addLineToPoint:element->points[1]];
			[line addLineToPoint:element->points[2]];
			[line stroke];
			[[UIColor greenColor] setStroke];
			[crossAtPoint(element->points[0]) stroke];
			[crossAtPoint(element->points[1]) stroke];
			[[UIColor orangeColor] setStroke];
			[crossAtPoint(element->points[2]) stroke];
			self.previousPoint = element->points[2];
		}
			break;
		case kCGPathElementCloseSubpath:
			break;
	}
}

@end

Have fun!