Wednesday, October 7, 2009

Rounded Corners on UITableView

The iPhone UI is a soft, graceful collection of rounded corners. Your application icon is cropped and smoothed out with the ubiquitous curves. The highlight on the selected tab of a tab bar has tiny ones. Grouped table views, the corners of navigation items, the little buttons in toolbars. You can't escape the smooth arcs.

Which would be fine if Apple didn't require you to hand craft them yourself on your UI components. You might expect a "draw this with rounded corners" method. Actually, given their prevalence, you'd expect it to be a setting on UIView: cornerRadius or something like that.

But since that doesn't exist, every developer has to write his or her own take on it. So far, mine has become fairly standard. My UIView classes implement drawRect to set a curvy clip region. I have a utility method that takes any rect, with a desired radius, and returns a path for a rounded rectangle. Add that to the graphics context, call CGContextClip, and voila, I have voluptuousness. This has worked well.

Then I tried to do it on a UITableView embedded in a larger view. It's a view, right? So this should work, right? Except that UITableView's drawRect method doesn't really do anything. The corners don't get clipped if you subclass it and clip within drawRect.

Searching on this topic suggests that this is a common problem. I read various ideas, didn't really like them for one reason or another, and then came up with my own strategy. Why not overlay my table with a view that would act as a window? It would draw the part that I normally clip off and then make the interior transparent. And with a bit of work, I could make it a reusable view I could put atop other views that didn't behave the way I wanted. No more mucking around with clipping in random subviews!

I am not a graphics programmer. Not only did this take me a while, but there are probably better ways to do it. However, you may find yourself wondering how to do the same thing, and so I'm sharing my results.

Here's my init method. I used bgColor to represent the color on the outside of the corners, and I set the actual backgroundColor to clearColor I tried using clearColor as the fill color for my rounded path, but that didn't work. It was transparent over the view's background color, which meant the whole rectangle was filled in with the view's background, not the view underneath.



- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
bgColor = [UIColor blackColor].CGColor;
self.backgroundColor = [UIColor clearColor];
self.userInteractionEnabled = YES;
}
return self;
}


And here's the drawing code. I used EOFillPath to ensure that my interior was empty. Yes, I hardcoded the radius. Yes, that's lame. I'll fix it in a refactor pass.


- (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef ctxt = UIGraphicsGetCurrentContext();

CGContextSetFillColorWithColor(ctxt,bgColor);
CGContextAddRect(ctxt,rect);
CGPathRef curvePath = [Utils roundedCornerPathFromRect:rect withRadius:10.0];
CGContextAddPath(ctxt, curvePath);
CGPathRelease(curvePath);

CGContextEOFillPath(ctxt);
[super drawRect: rect];
}


That's the result of a fair amount of fiddling, and it now works. Except for one problem: You've covered the view you want the user to see with another view, effectively removing the user's ability to interact with it. To get around this, I created a "coveredView" reference within RoundedCornerOverlay and set it in the view controller managing the whole view. I overrode nextResponder in my view to return this reference. Then I overrode the various event methods you inherit from UIView. The final step was to realize I needed to convert incoming points to be relevant within the covered view.



- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[[self nextResponder] touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[[self nextResponder] touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[[self nextResponder] touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[[self nextResponder] touchesCancelled:touches withEvent:event];
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return [[self coveredView]
hitTest:[self convertPoint:point toView:[self coveredView]] withEvent:event];
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return [[self coveredView]
pointInside:[self convertPoint:point toView:[self coveredView]] withEvent:event];
}

Saturday, October 3, 2009

Independent Developer

I assume that if anyone reads this blog — anyone? hello? — they also read my main blog, An Obsession With Food. So you've probably already seen the announcement: I launched 1.0 of my iPhone app, which helps home cooks organize the prep tasks for a dish.

What that really means is that for the first time in my professional career, I'm an independent developer. Of course, I also work for a company that can pay me. I am not planning my retirement based around sales of my application in the App Store. But I'll be the one doing the marketing — ugh — tech support, and project management. Now I'll have users. At least I hope I will.

So far, I've been the primary user of this app. In a way, this is good, because it's forced me to use the app day in and day out, finding the things that annoy me and then fixing them. But it's also bad. The app is tailored to the way I do things, even though most people probably have different menu planning strategies. I've had testers, of course, who have offered feedback. But, as of today, I have four times as many users as I did before I launched. I'm hoping they'll take the time to write me and offer suggestions, but who knows what will happen?