i’m so full of ideas

Drawing NSStrings in unusual rotations

June 1, 2009 8:10 pm

If you’re used to writing apps for desktop computers, the iPhone’s screen can seem awfully small. You must make creative use of every pixel available to you. One way to do that is to draw some text vertically, rather than horizontally, which I decided to do for an iPhone app I’m working on right now.

For drawing normal horizontal text, UIKit has a category called UIStringDrawing that provides convenient methods for drawing the contents of an NSString to the current graphics context, like drawAtPoint:withFont: and drawInRect:withFont:. Sadly, UIKit does not provide methods for drawing text in any other orientation except standard horizontal. I will now present an NSString category you can add to your own iPhone projects that allows you to draw vertical text from bottom to top, top to bottom, or horizontal text upside down.

My category supports the same three UITextAlignment options that UIKit provides for left, right, or centered text alignment. If using my drawInRect:... method to draw a string that’s too wide to fit within the rectangle supplied, the right end of the string will be truncated to fit, with an ellipsis character added.

UIKit does a better job of handling long strings than my code does. UIKit can wrap text to two or more lines, for example, which I did not try to emulate. The UIKit methods also allow you to provide a UILineBreakMode enum value, which gives you fine-grained control over how strings are truncated. I personally don’t need that much flexibility, so my category has no such support.

Finally, there is one last limitation of my category that might be a deal-breaker for you. The standard UIKit string-drawing functions can be used to display any Unicode character, so long as the font you’re using has a glyph for it. My category is limited to the roughly 255 characters present in the MacRoman character set. That means my code is good for displaying text in English and most European languages, like French and German, but it is completely unsuitable for text in, say, Chinese.

I’m aware that this is an outrageous limitation. You’d be hard-pressed to find a programmer who is more gung ho about Unicode than I am. I struggled mightily for several days trying to find a way around this. Sadly, this is due to the way that the underlying CoreGraphics drawing routines work, and there is no good way around it that I can see. I’ll cover this in more detail later, in case you’re interested.

Using the WBTextDrawing category

I’ve included a sample Xcode project that demonstrates the use of my WBTextDrawing category. Download the project by clicking here.

I’ve provided a screen-shot, but it isn’t much to look at. There are five strings displayed onscreen. The four strings at the edges of the view are displayed with my own WBTextDrawing category, drawn in all four available orientations. The string in the center is drawn with one of UIKit’s own drawInRect:... methods. The lines in red show the bounding rects used to draw the five strings. The four strings around the edges all have an associated green pixel, which illustrates the draw point used to draw that particular string. Naturally you wouldn’t draw the green and red bits in a real app. I added them to this demo so you’ll have a better idea of what’s going on.

Almost all the code in the project is boilerplate that can be safely ignored. To add my string-drawing category to your own program, copy WBTextDrawing.mm and WBTextDrawing.h out of this project and into your own. All other source files presented here are for demonstration purposes only.

You will note that WBTextDrawing.mm ends with an mm extension, rather than the usual m. That’s because this source file must be compiled as Objective-C++, due to the fact that it contains a small amount of C++. I know many Objective-C programmers have a strong aversion to C++, and believe me, I understand! But the underlying CoreGraphics method that’s used to draw rotated strings insists on being given a const char*. It does not work with NSString objects directly. So I chose to convert NSString objects to std::string objects just before drawing them. Yes, I could have accomplished this without leaving the confines of Objective-C, but std::string seems to me like the best tool for the job.

Specifying text drawing locations

UIKit’s UIStringDrawing category contains several drawAtPoint:... methods for drawing NSString objects. The point you pass to these methods is the far left end of the font’s baseline, as illustrated by the green point in the first figure. For the sake of compatibility, I chose to use this same convention for the drawAtPoint:... method in my own WBTextDrawing category. No matter what drawing orientation you’re using, the specified draw point is always the far left end of the font’s baseline, relative to the string being drawn. This is also the way the low-level CoreGraphics drawing routines work, conveniently enough. See the second figure for what this looks like when drawing a bottom-to-top vertical string.

UIStringDrawing also has several drawInRect:... methods. In this case, the rectangle supplied is the entire area allotted for drawing the string, illustrated by the red rectangles in figure one and figure two. Again, my own WBTextDrawing category does the exact same thing, for compatibility’s sake.

Text drawn is limited to MacRoman

This is without a doubt the worst limitation of my WBTextDrawing category. I see no good way around it, however.

Whatever method you use for drawing rotated text, you must accomplish two goals: 1) Apply a given font to the current drawing context, and 2) Draw the NSString supplied by the caller. The easiest way I can see to apply a font is to use CGContextSelectFont(). That method gives you two encoding options: kCGEncodingMacRoman, which uses the MacRoman encoding, or kCGEncodingFontSpecific, which means you must provide your own character-to-glyph translations, as far as I can tell. Ahem. UIKit can’t do this by itself, apparently, but the expectation is that us lowly app programmers should be able to do it? As if. So the only real option here is to use the MacRoman encoding, then use CGContextShowText() to display the text. This is the way my category works. Anything outside the MacRoman character set displays as gibberish.

There’s another method you can use to apply a font to the current context: CGContextSetFont(). This method doesn’t take any kind of encoding parameter at all, so it would appear to be immune from the problems presented by CGContextSelectFont(). Alas, once you’ve called that method, CGContextShowText() doesn’t work anymore. The CoreGraphics docs say you should instead call CGContextShowGlyphsAtPoint(). That function expects its caller to supply an array of glyphs, not characters. Which implies that you’ve got some method up your sleeve that will convert characters to glyphs for a given encoding. I don’t know about you, but I don’t have any such method lying around. So I’m stuck with boring old MacRoman.

This seems like an awfully strange limitation to build into the low-level CoreGraphics text drawing routines. If you know of any way around it, please tell me what it is, so I can update my text-drawing category appropriately.

No Responses to “Drawing NSStrings in unusual rotations”