i’m so full of ideas

Archive for April, 2009

iphone: drawing rectangles

April 28, 2009 9:33 am

Mac/AppKit and iPhone/UIKit development are more alike than different. In general, I’d say the iPhone APIs feel more “fresh” than their Mac equivalents. It seems to me like the current crop of Cocoa engineers at Apple have learned a lot in the years since the company ditched “classic” MacOS for an updated version of NextStep, and they’ve used that wisdom to design this new crop of frameworks. And they’re not afraid to revisit past decisions that didn’t work out so well: they discarded MacOSX’s view-origin-in-the-lower-left-corner mistake and put it in the upper left corner for the iPhone, where it belongs.

On the other hand, AppKit is far more complete than UIKit. Almost anything user-interface-related you might want to do on the Mac is covered by APIs in AppKit. UIKit is missing many of those amenities, likely because it is targeted at resource-constrained devices that don’t have the space for dozens of multi-megabyte frameworks. The practical upshot is that I’ve spent a lot of time writing iPhone stuff that AppKit would have provided for me. All the more reason to write blog posts like this one, so coders can google up the answers rather than spending days reinventing wheels.

I like drawing rectangles. Only rarely do they end up in my final apps, but I use them a lot while debugging, to ensure that a view area I’ve staked out is where it should be. On the iPhone, this kind of thing is done with Core Graphics. It is a very complete framework. You can do anything with it. The down side is that CG code tends to be verbose and complicated, requiring lots of function calls to do the simplest things. This is a situation crying out for wrapper functions.

I’ve written a UIView category that provides methods for drawing just about any type of rectangle you can think of: filled or outlined, rounded corners or square, including point-drawing methods. You can provide a color to draw with, or use the current draw color, which is black, unless you change it. You can draw a translucent rectangle by specifying a draw color that has an alpha value of less than one. You can change the radius of the rounded corners drawn by fiddling with the constant kCornerSize at the top of the implementation file.

This is beginner-level stuff, so I guess I should add that UIKit doesn’t let you draw things whenever you feel like it. (That’s true for all GUI environments I’ve ever programmed for, now that I think about it.) You must draw only within a UIView’s drawRect: method. If this is news to you, you should read one of Apple’s introductory texts before continuing.

Here’s a sample drawRect: method you can add to an existing view to demonstrate the rect-drawing methods:

-(void)drawRect:(CGRect)rect {     CGRect  test = CGRectMake(0.0, 0.0, 20.0, 20.0);     [self strokeRect:test color:[UIColor magentaColor]];     test.origin.x += test.size.width;     [self strokeRoundRect:test color:[UIColor yellowColor]];     test.origin.x += test.size.width;     [self fillRect:test color:[UIColor blueColor]];     test.origin.x += test.size.width;     [self fillRoundRect:test color:[UIColor redColor]]; }

… which produces the output shown in the screen-shot. here’s the header file for the category:

// WBViewRect.h -- rectangle drawing methods for UIView // by allen brunson  march 2 2009 #ifndef WBVIEWRECT_H #define WBVIEWRECT_H #import <UIKit/UIKit.h> @interface UIView (WBViewRect) // save and restore graphics context -(void)contextRestore:(CGContextRef)context; -(CGContextRef)contextSave; // points -(void)drawPoint:(CGPoint)point; -(void)drawPoint:(CGPoint)point color:(UIColor*)color; // filled rects -(void)fillRect:(CGRect)rect; -(void)fillRect:(CGRect)rect color:(UIColor*)color; // filled rects with rounded corners -(void)fillRoundRect:(CGRect)rect; -(void)fillRoundRect:(CGRect)rect color:(UIColor*)color; // outlined rects -(void)strokeRect:(CGRect)rect; -(void)strokeRect:(CGRect)rect color:(UIColor*)color; // outlined rects with rounded corners -(void)strokeRoundRect:(CGRect)rect; -(void)strokeRoundRect:(CGRect)rect color:(UIColor*)color; @end #endif  // WBVIEWRECT_H

Finally, here’s the implementation file:

// WBViewRect.mm -- rectangle drawing methods for UIView // by allen brunson  march 2 2009 #include "WBViewRect.h" #pragma mark module data static const CGFloat kCornerSize = 5.0; static CGRect rectStrokeAdjust(CGRect rect) {     rect = CGRectIntegral(rect);     rect.origin.x    += 0.5;     rect.origin.y    += 0.5;     rect.size.width  -= 1.0;     rect.size.height -= 1.0;     return rect; } static void roundRect(CGContextRef context, CGRect rect,  CGFloat ovalWidth, CGFloat ovalHeight) {     CGFloat  fw = 0.0;     CGFloat  fh = 0.0;     assert(ovalWidth  >= 1.0);     assert(ovalHeight >= 1.0);     CGContextSaveGState(context);     CGContextTranslateCTM(context, CGRectGetMinX(rect), CGRectGetMinY(rect));     CGContextScaleCTM(context, ovalWidth, ovalHeight);     fw = rect.size.width  / ovalWidth;     fh = rect.size.height / ovalHeight;     CGContextMoveToPoint(context, fw, fh / 2.0);     CGContextAddArcToPoint(context, fw, fh, fw / 2.0, fh, 1.0);     CGContextAddArcToPoint(context, 0.0, fh, 0, fh / 2.0, 1.0);     CGContextAddArcToPoint(context, 0.0, 0.0, fw / 2.0, 0, 1.0);     CGContextAddArcToPoint(context, fw, 0.0, fw, fh / 2.0, 1.0);     CGContextClosePath(context);     CGContextRestoreGState(context); } @implementation UIView (WBRectView) -(void)contextRestore:(CGContextRef)context {     CGContextRestoreGState(context); } -(CGContextRef)contextSave {     CGContextRef  ctxt = UIGraphicsGetCurrentContext();     CGContextSaveGState(ctxt);     return ctxt; } -(void)drawPoint:(CGPoint)point {     [self drawPoint:point color:nil]; } -(void)drawPoint:(CGPoint)point color:(UIColor*)color {     CGRect  rect = CGRectMake(point.x, point.y, 1.0, 1.0);     [self fillRect:rect color:color]; } -(void)fillRect:(CGRect)rect {     [self fillRect:rect color:nil]; } -(void)fillRect:(CGRect)rect color:(UIColor*)color {     CGContextRef  ctxt = [self contextSave];     if (color)     {         CGContextSetFillColorWithColor(ctxt, [color CGColor]);     }         UIRectFill(rect);     [self contextRestore:ctxt]; } -(void)fillRoundRect:(CGRect)rect {     [self fillRoundRect:rect color:nil]; } -(void)fillRoundRect:(CGRect)rect color:(UIColor*)color {     CGContextRef  ctxt = [self contextSave];     roundRect(ctxt, rect, kCornerSize, kCornerSize);     if (color)     {         CGContextSetFillColorWithColor(ctxt, [color CGColor]);     }         CGContextFillPath(ctxt);     [self contextRestore:ctxt]; } -(void)strokeRect:(CGRect)rect {     [self strokeRect:rect color:nil]; } -(void)strokeRect:(CGRect)rect color:(UIColor*)color {     CGContextRef  ctxt = [self contextSave];     if (color)     {         CGContextSetStrokeColorWithColor(ctxt, [color CGColor]);     }         UIRectFrame(rect);     [self contextRestore:ctxt]; } -(void)strokeRoundRect:(CGRect)rect {     [self strokeRoundRect:rect color:nil]; } -(void)strokeRoundRect:(CGRect)rect color:(UIColor*)color {     CGContextRef  ctxt = [self contextSave];     rect = rectStrokeAdjust(rect);     roundRect(ctxt, rect, kCornerSize, kCornerSize);     if (color)     {         CGContextSetStrokeColorWithColor(ctxt, [color CGColor]);     }         CGContextStrokePath(ctxt);     [self contextRestore:ctxt]; } @end

iPhone: UITextField and the virtual keyboard, part II

April 20, 2009 8:42 pm

I’ve written about this subject before, but I didn’t do a very good job. Many weeks of intensive iPhone programming later, I am ready to deliver my definitive treatise on this subject.

The Problem

Say you’ve got a UITextField that you want the user to type something into. It’s located near the bottom of the screen. The user touches the control to begin editing. The virtual keyboard pops up, which completely covers the UITextField. It is now impossible for the user to see what she’s typing. You have to write your own custom code to move the text field out of the way before the keyboard pops up, so the user can see it.

This is against the spirit of Cocoa. AppKit on the Mac and UIKit on the iPhone normally give you decent behavior by default, and lots of hooks you can use for customization, if the defaults don’t suit your situation. In this case, they gave us a big problem by default and very little guidance towards solving it. Apple’s own example code does not do a good job with this. What we’re left with is lots of blog authors like me writing half-baked solutions to partial subsets of the problem, whereas Apple could have solved all of it, easily. Disappointing.

keyboardscroll screen-shot

keyboardscroll screen-shot

Having lived with this for awhile, I thought about how best to write something definitive, so I don’t have to keep tripping over this issue again and again. The solutions I’ve seen on programming forums and blogs involve code snippets you can drop into an existing view controller, but you have to do so every time you write a new view that contains text fields. The height of cut-and-paste code reuse.

My first idea was to add a category to UIViewController. Then every new view controller I write from now on would get textfield-handling ability “for free.” But Apple has already solved this problem for the UITableView object with special code inside UITableViewController, as of iPhone OS version 2.2. It’s not right to apply my fix to every view controller if some don’t need it. So instead I implemented a descendant of UIViewController, which can be derived from to create new view controllers.

The Solution

My keyboardscroll Xcode project shows you two ways you can solve this problem. If you’re using a UITableView, the issue is solved for you by UITableViewController, if you get all the connections right. I’ve included a table view controller that works this way, because it was harder than I thought it would be. My first attempt failed miserably. If you’re using some other view type, you can write a new view controller descended from my own WBKeyboardViewController class, which the example project shows you how to do.

My view controller works for views that contain any number of UITextFields. The content view can be a UIView or UIScrollView, either works fine. Both portrait and landscape modes are fully supported.

The only serious limitation is that my code doesn’t cope with UITextViews, which the user can also type into, and are subject to the same problem. I don’t have enough experience with text views to have a good idea about how to approach that problem yet. My first guess is that my view controller could be made to deal with them fairly easily, with a few special cases here and there.

To the greatest extent possible, I’ve avoided hard-coding pixel counts. For example, the portrait mode virtual keyboard is 216 pixels tall, and the landscape one is 162 — but those two numeric constants do not appear anywhere in the code. The proper values to use are retrieved from the operating system. Apple’s own UICatalog sample project hard-codes the height of the keyboard, so I didn’t discover that there is a better way until I had been searching for articles about this for some time.

Hard-coded pixel counts are not just a theoretical concern. “In iPhone OS 1.1.4 and earlier, the keyboard height in landscape orientation was 180 pixels.” Straight from Apple’s own documentation. Of course, that was from before the iPhone SDK existed. I think they’d be reluctant to make such a change today, since it would break so many third-party apps. Better safe than sorry, though.

There are 40 total UITextFields in my example project spread over two views, and they always, always get out of the way before the keyboard appears. I’ve tested this code thoroughly in the simulator. I’ll be using it on a real iPhone soon, so if there are issues in that scenario, I’ll find them and update this blog post. There are no bugs that I’m aware of. If you find one, I’d love to hear about it, so I can fix it.

Landscape Mode is the Devil’s Own Handiwork

Whew, this was way more complicated than I thought it would be. Apple chose to implement the landscape modes far differently than I would have. I say “modes” rather than “mode,” because there are two of them. The iPhone’s user interface can be rotated to one of four orientations: portrait, landscape, portrait upside down, and landscape upside down.

When your view rotates into landscape mode, almost all of UIKit remains stuck in portrait mode. That means you have to write lots of special cases. Here’s the first example. This code snippet gets you the bounds rect of the iPhone’s screen:

    CGRect rect = [[UIScreen mainScreen] bounds];

In portrait mode, the rect returned has a height of 480 pixels and a width of 320, which is correct. In landscape mode, the rect returned is exactly the same as the one you get in portrait mode. Erm. Okay, it’s easy enough to write a wrapper function that swaps the rect’s width and height when appropriate, which I did.

Next problem. UIView contains methods for converting rects and points from the coordinate system of one view into another, convertRect:toView: being one of them. I’ve successfully used those methods in portrait modes, but not in landscape modes. It appears to me that, while your view is in landscape mode, the main window remains in portrait mode, so translations between the two are nonsensical. I couldn’t make it work, anyway. Could be because they’re weird in the way I just described, or it could be because I don’t know what I’m doing. Either way, I had to abandon UIView’s conversion methods and roll my own.

The next problem I ran into indicates the UIKit’s designers’ intent quite strongly. UIApplication has a property called statusBarFrame. Normally I wouldn’t need to know the size of the status bar, but I was forced to roll my own coordinate conversion methods, remember. In portrait mode, the property returns a width of 320 pixels and a height of 20 pixels, which is correct. In landscape mode, it says the status bar is 20 pixels wide and 480 pixels tall! That’s obviously wrong, from the perspective of your landscape-mode view. It would be correct only from the perspective of a parent view that’s still in portrait mode. Yet another special case I had to write for the landscape modes.

It took me three days to get my view controller working right, despite the fact that it is only around 250 lines of code. Largely because I have come to trust that UIKit will Do The Right Thing in almost every case, so I could barely conceive of the idea that it would ever tell me that the status bar is 480 pixels tall with a straight face. Not at all what I was expecting, so I had to abandon a lot of early code and rethink my assumptions.

Here’s the code: keyboardscroll.zip

details

April 9, 2009 8:15 am

As of this writing, a google search for “UIImage rotate” turns up my previous blog article as the third hit. If you ask me, my take on the subject is far more useful than the top two hits. Yes, I am a shameless braggart, thanks for noticing.

The reason I wanted to rotate UIImages is because I’m working on an iPhone card game, soon to be revealed. While casting about the ‘net for playing card images to use, I came across this excellent page at www.jfitz.com. There you will find the original Windows-3-era playing card images, made famous by Windows Solitaire, everyone’s favorite time-waster. I can recall whiling away countless hours with that game myself, back when I was a Windows user. One day in the mid nineties I was poking around in my Windows directory and came across CARDS.DLL, which I opened up with the resource editor that came with Microsoft’s compiler of that era, and discovered the bitmaps for all those playing cards lurking inside. I spent ten minutes contemplating writing a Windows card game, then promptly forgot about it. I didn’t know until recently that those iconic card images were created by the legendary Susan Kare, the woman who earlier made the refreshing, tiny black-and-white icons for the original Macintosh. Check out the one she made depicting Steve Jobs. It captures the essence of him in a mere 32 bytes.

Susan Kare’s Windows playing cards turned out to be perfect for my game. Their dimensions are close to ideal, given the size of the iPhone’s screen. If they were any bigger, you wouldn’t be able to fit enough of them onscreen at once. Any smaller and the player would have trouble selecting individual cards with her fingers.

Ugly green pixel

Ugly green pixel

I used these graphics for a couple of days and then noticed a problem. The card images all have an unnecessary green pixel in each corner. I originally planned to work around it by using that same shade of green for my background, so that the unwanted pixels would be “invisible.” That works okay, sort of, until you draw cards on top of other cards. I refer you now to the picture I’ve included with this post. Look at that! Ugly green pixel in the midst of an otherwise pristine sea of retro Windows-3-era opulence!

The wise move would be to ignore it. I have more work to do than I can finish in this lifetime. It’s a minor detail that can wait until after I find out if anybody will ever play this thing. Sadly, this is me we’re talking about. One thing I don’t like about myself is my inability to ignore minor junk like this and focus on the bigger picture. But there it was in my head as I was working on this, over and over: ugly green pixels, ugly green pixels.

The proper fix is that the pixels that are now green need to be made transparent. I recently bought a license for Acorn, specifically to do programming-related graphics work such as this. Acorn may be capable of making a single pixel transparent, but if so, I can’t figure out how.

I evaluated other options. Make the green pixels transparent at runtime, just before displaying the images? A quick scan of the docs told me that UIImage doesn’t make it very easy. Try a different image editor? I downloaded a couple others, which were downright impenetrable, compared to Acorn. I had a look at the docs for libpng, thinking I might write a one-off utility to transparent-ize the pixels. libpng is a great piece of software, but I quickly determined that it would take me days to figure out the API well enough to do what I want.

You’re bored so let’s cut to the chase: the solution was ImageMagick. I was turned off initially, because it has that faint musty unix smell that makes me fear I’m in for trouble. I wasn’t wrong! I tried to install it via MacPorts, which gave me a fresh new version of the usual rambling useless error messages it always gives me. Maybe it thought my X11 version was too old? As if I really want to spend all day figuring out if that is in fact what it’s upset about, or how I would install a later version of X11 that I’m never going to use. So I downloaded a prebuilt binary distribution from the ImageMagick site, which was, predictably, a pain in the ass to set up. Unix motto: making simple things tedious and unpleasant for four decades!

Have to give them props, though: ImageMagick is a powerful tool. Thirty minutes of poking and prodding later, I devised this bash script to convert my entire folder-full of images at once:

#!/bin/sh for file in $(ls *.png); do     echo "converting $file ..."     convert $file -transparent "rgb(0,128,0)" $file done

Problem solved. Drawing the new-and-improved images in the iPhone simulator does exactly the right thing. The formerly-green pixels are now invisible. I tried drawing a UIImage directly and also putting it inside an UIImageView, both methods work perfectly.

The practical upshot is that my stupid brain can finally stop fixating on this minor detail, and hopefully I can get back to work.