Wednesday, March 4, 2009

UIRoundedView

Today I wanted to add a little view to my iPhone app, to display some text (an about box). However, I wanted it to look a little more interesting than just a plain square view... perhaps a box with nicely rounded corners!


This is pretty easy to do using the iPhones SDK. The first thing we need to do is subclass UIView so we can make our rounded UIView, so lets create a header which looks like this:



#import <UIKit/UIKit.h>


@interface UIRoundedView : UIView {
}
@end

Easy! But not very useful yet. Lets have a look at the code side of things. The important member of UIView we need is drawRect:


- (void)drawRect:(CGRect)rect {
int radius = 10;

CGContextRef context = UIGraphicsGetCurrentContext();
[self drawRounded:rect With:radius On:context];
CGContextFillPath(context);
}

The drawRect function will get called anytime this view is updated. The drawRounded function contains the actual code, but lets break it down piece by piece. Firstly, we start the path. This is tells the drawing code, be it drawing a line, or filling a shape, where the bounds are.

// Get the current drawing context
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextBeginPath(context);

Next up, we set a starting point


CGContextMoveToPoint(context, x1, y1);

And then for the corner:


CGContextAddArcToPoint(context, cx, cy, x2, y2, radius);

It needs to be noted that (cx,cy) and (x1,x2) are not the same point. In fact, (cx,cy) is not a point on the line at all. The current position (which we set with CGContextMoveToPoint() and is currently (x1,x2) ) and (x2,y2) will form the end points of the arc.

Next is a simple matter of drawing a line from the 'current' position, which is updated by CGContextAddArcToPoint(), to
the start of the next corner curve:


CGContextAddLineToPoint(context, x1, y1);

Now, lets look at the whole function. Rather than type this out 4 times, I have placed it in a loop.


-(void)draw: (CGRect)rect With:(float) radius On:(CGContextRef) context {
float x1,x2,y1,y2; // x1,y1 is the starting co-ord, x2,y2 is the end point
float cx, cy; // The tangent intersect point
corner_t corner;

CGContextBeginPath(context);

for(int i = 0; i < 4; i++) {
corner = i;

switch (corner) {
case topleft:
x1 = rect.origin.x;
y1 = rect.origin.y + radius;

cx = rect.origin.x;
cy = rect.origin.y;

x2 = rect.origin.x + radius;
y2 = rect.origin.y;
break;
case topright:
x1 = rect.origin.x + rect.size.width - radius;
y1 = rect.origin.y;

cx = rect.origin.x + rect.size.width;
cy = rect.origin.y;

x2 = rect.origin.x + rect.size.width;
y2 = rect.origin.y + radius;
break;
case bottomright:
x1 = rect.origin.x + rect.size.width;
y1 = rect.origin.y + rect.size.height - radius;

cx = rect.origin.x + rect.size.width;
cy = rect.origin.y + rect.size.height;

x2 = rect.origin.x + rect.size.width - radius;
y2 = rect.origin.y + rect.size.height;
break;
case bottomleft:
x1 = rect.origin.x + radius;
y1 = rect.origin.y + rect.size.height;

cx = rect.origin.x;
cy = rect.origin.y + rect.size.height;

x2 = rect.origin.x;
y2 = rect.origin.y + rect.size.height - radius;
break;
default:
break;
}

// Start the path if its the first iteration
if(i == 0)
{
CGContextMoveToPoint(context, x1, y1);
}
else
{
CGContextAddLineToPoint(context, x1, y1);
}

// Draw the corner arc
CGContextAddArcToPoint(context, cx, cy, x2, y2, radius);
}

CGContextClosePath(context);
}

So now we have a UIView that fills itself... however we haven't specified a colour, and worse, if we set a background colour in InterfaceBuilder, we are going to see that and its going to have square edges! So we need to do a bit more work. We need to override backgroundColor. We need to do this, because we want the base UIView's backgroundColor to always be [UIColor clearColor].

So lets override setBackgroundColor and backgroundColor:


-(void)setBackgroundColor: (UIColor *) color {
self.fillColor = color;
[self setNeedsDisplay];
}

-(UIColor *)backgroundColor {
return self.fillColor;
}

Note that I have called [self setNeedsDisplay], this will ensure that if at any point you assign a new background colour
to the view, it will get updated. The other thing I want to add is a border. The easiest way to do this, is simply call our drawing
code twice:


- (void)drawRect:(CGRect)rect {
int radius = 30;

CGContextRef context = UIGraphicsGetCurrentContext();

[fillColor setFill];
[self draw:rect With:radius On:context];
CGContextFillPath(context);

CGContextSetLineWidth(context, borderWidth);
[borderColor setStroke];
[self draw:rect With:radius On:context];
CGContextStrokePath(context);

}

Now a member for borderColor as well, and we have a useful UIRoundedView class!


One small gotcha to be aware of, is that setting defaults in initWithFrame() isn't going to work if you are using IB to create your objects. Use initWithCoder instead.

And here is the result!


Source available: UIRoundedView.m and UIRoundedView.h

2 comments:

  1. When I programatically add the UIRoundedView as a subview of my UITableViewController's view, I get the rounded rect but over a black square background.

    You mentioned needing to use clearColor for the background but I don't see that anywhere in your code. What do I need to do to make the background clear?

    ReplyDelete
  2. Hmm, in my test cases I did not explicitly need to set the super backgroundColor to clear, however, inside a table it is definitely necessary.

    Add this to your initWithFrame() function:

    super.backgroundColor = [UIColor clearColor];

    ReplyDelete