Monday, September 24, 2012

On Vectored Drawing In iOS Interfaces

As someone who spent spent seven years of his life writing a vectored drawing app, I have a deep rooted dislike of the bitmap. Bitmaps are bulky, bitmaps are inflexible. Bitmaps are a general pain. And yet oftentimes, it seems like many iPhone apps are drawn out entirely out of Photoshop.

Take this simple artwork from my iPhone app Signal GH. It formed the background of the graph forming the top portion of the app. I had drawn it in Photoshop Elements in less than 20 minutes.

And formed as in past tense, as I ripped it out this weekend as part of getting the app ready for the new 4 inch displays. Why, let's count the reasons.

  1. In this particular interface, the graph was the flexible element that made use of extra space when available and gave up space when needed. So, I would have needed a separate version for the iPhone 5/new iPod touch.
  2. I had not been handling the shrinking of the interface when a phone call comes in and the big green band comes down. Yet another flexible size needed.
  3. I needed artwork for Retina and non-retina, iPhone and iPad. With the addition of the retina iPhone, that's 5 different variants. 
  4. Should I decide to localize the app, the axis labels "signal quality" and "time" would require new versions per localization so 5n where n is the number of localizations. 
  5. It took up space. Not much but some. I take pride in that of the hundreds of apps on my phone, my apps, TV Towers USA and Signal GH are the second and third smallest behind an old flashlight app. 
So out went the UIImageView backdrop and in came a custom view with 3 subviews for the left axis, bottom axis and the graph itself, all lain out in the custom view's layoutSubviews handler. Here's the code for drawing the left axis:
@implementation LeftAxis
-(void)drawRect:(CGRect)rect
{
    CGFloat axisHeight = GetAxisHeight();
    CGFloat axisWidth = GetAxisWidth();
    CGRect myBounds = self.bounds;
    UIColor* strokeColor = [UIColor colorWithWhite:kGrayLevel alpha:1.0];
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSaveGState(context);
    CGContextSetLineJoin(context, kCGLineJoinMiter);
    
    CGFloat myWidth = myBounds.size.width;
CGFloat myHeight = myBounds.size.height;
CGContextMoveToPoint(context,myWidth, myHeight-(axisHeight-axisWidth)); CGContextAddLineToPoint(context, myWidth-kShaftWidth, myHeight-(axisHeight-axisWidth)); CGContextAddLineToPoint(context, myWidth-kShaftWidth, myHeight-axisHeight+kShaftWidth); CGContextAddLineToPoint(context, 0.0, myHeight-axisHeight+kShaftWidth); CGContextAddLineToPoint(context, 0.0, myHeight-axisHeight); CGContextAddLineToPoint(context, myWidth-kShaftWidth, myHeight-axisHeight); CGContextAddLineToPoint(context, myWidth-kShaftWidth, kArrowHeadLength); CGContextAddLineToPoint(context, myWidth-kArrowHeadWidth, kArrowHeadLength); CGContextAddLineToPoint(context, myWidth, 0); CGContextClosePath(context); CGContextSetFillColorWithColor(context, strokeColor.CGColor); CGContextFillPath(context); UIFont* textFont = [UIFont systemFontOfSize:kAxisFontSize]; NSString* signalQualityString = 
NSLocalizedString(@"signal quality", @"signal quality label on left axis");
     CGSize textSize = [signalQualityString sizeWithFont:textFont];
    
    CGPoint drawPoint = CGPointMake(myWidth-kShaftWidth-textSize.height,
                                    kArrowHeadLength+kArrowHeadWidth+textSize.width);

    CGContextTranslateCTM(context, drawPoint.x, drawPoint.y);
    CGContextRotateCTM(context, -1.0*M_PI_2);
    [strokeColor set];
    [signalQualityString drawAtPoint:CGPointZero withFont:textFont];
    CGContextRestoreGState(context);
}
@end

Notice that I draw the entire y-axis, including the protrusion of the x-axis in one continuous path using a single filled polygon. You might be tempted to draw the line segments as individual stroked lines and only fill the arrowhead, but experience tells me that as long as the color is the same throughout, there will be fewer problems if I just fill one contiguous path. For example, you won't get anti-alias artifacts between adjacent elements. Also note that the text is localized and positioned by its measured size and not a magic number. 

The net win here is that I don't have to keep multiple artwork up to date, my app is smaller, I can localize. The operating system can bring down the call bar without distortion, and I'm drawing at the intended resolution of the screen.