Ten months ago when the original iPad shipped, Apple released iOS 3.2, and for the first time, iOS developers had access to
NSAttributedString and
NSMutableAttributedString, objects designed to hold strings along with font, paragraph, and style information. We no longer had to resort to using heavy
UIWebViews or complex Core Graphics calls to draw styled text.
Well, sort of…
On the Mac side of things,
NSAttributedString and its counterpart
NSMutableAttributedString have been around for a long, long time, as part of Foundation. But, there's also been, for nearly as long, categories on both of these classes in App Kit called the
Application Kit Additions which have all sorts of useful additional methods.
These categories provide ways to create attributed strings from various sorts of formatted text documents (RTF, HTML), to create attributed strings by specifying multiple specified attributes, to tweak existing attributes, to draw the attributed string, and to determine the size of an attributed string if it were to be drawn.
In fact, most of the really useful methods for these two classes are contained in these App Kit categories and not in the base classes. Unfortunately, we don't have those categories in the iOS SDK, or even a scaled back version of them. We just have the base classes. That means we have a whopping thirteen methods on
NSAttributedString, and another thirteen on
NSMutableAttributedString.
Cocoa has luxury-brand attributed strings; Cocoa touch has store-brand generic ones.
Even weirder,
NSAttributedString has an init method that takes a dictionary of string attributes, but the key constants for using that method aren't even included in iOS in either the public headers or the documentation. The description of the methods that take these attributes state that the constants are in the
Overview section of the documentation, but that's actually only true in the Mac OS X documentation, not the iOS documentation.
In other words, you can't create an
NSAttributedString or
NSMutableAttributedString using
initWithString:attributes: because you don't have the constants you need in order to specify the various attributes. That's not entirely true; you actually are able to use the Core Text counterparts of the
NSAttributedString constants , such as
kCTForegroundColorAttributeName in place of
NSForegroundColorAttributeName, however this isn't actually documented anywhere, and there isn't an exact 1:1 correlation between the
NS and
CT string attributes (though it's close).
This situation is really odd. Apple went through great efforts to give us all the low-level pieces need to do complex text rendering, but didn't give us higher-level objects to handle most that functionality elegantly. We have the lion's share of all of the low-level Core Text and Core Graphics calls that are available on Mac OS X (still no Core Image, though). Yet, we have to write low-level Core Text and Core Graphics code to do the bulk of even the most common typesetting tasks using attributed strings.
Fortunately,
NSAttributedString and
NSMutableAttributedString are both
toll-free bridged to their Core Foundation counterparts
CFAttributedStringRef and
CFMutableAttributedStringRef respectively. That means you can create, for example, a
CFAttributedStringRef and simply cast it to an
NSAttributedString pointer, and then calling
NSAttributedString methods on it will work.
Mostly.
There's one gotcha here. On iOS,
UIFont and
CTFont are
not toll-free bridged, even though
NSFont and
CTFont on the Mac are. You cannot pass a
UIFont into a function that expects a
CTFont and vice versa.
To get a
CTFont from a
UIFont, you can do this:
CTFontRef CTFontCreateFromUIFont(UIFont *font)
{
CTFontRef ctFont = CTFontCreateWithName((CFStringRef)font.fontName,
font.pointSize,
NULL);
return ctFont;
}
Notice the name of this method - the word "create" in the function name indicates that the returned
CTFont object has been retained for you, and you are responsible for calling
CFRelease() on it when you're done with it, to avoid leaking.
Going the other way, from a
CTFont to a
UIFont is only a little more involved. Here's a category method on
UIFont that will create an instance of
UIFont based on a
CTFontRef pointer
@implementation UIFont(MCUtilities)
+ (id)fontWithCTFont:(CTFontRef)ctFont
{
CFStringRef fontName = CTFontCopyFullName(ctFont);
CGFloat fontSize = CTFontGetSize(ctFont);
UIFont *ret = [UIFont fontWithName:(NSString *)fontName size:fontSize];
CFRelease(fontName);
return ret;
}
@end
Once you have the ability to convert the two font objects into each other, creating attributed strings really isn't that bad. Here's an example category method on
NSMutableAttributedString that will create an instance by taking an
NSString plus a font, a font size, and a constant representing the desired text justification. It will return an autoreleased attributed string with the text attributes applied to the entire string:
+ (id)mutableAttributedStringWithString:(NSString *)string font:(UIFont *)font color:(UIColor *)color alignment:(CTTextAlignment)alignment
{
CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
if (string != nil)
CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), (CFStringRef)string);
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTForegroundColorAttributeName, color.CGColor);
CTFontRef theFont = CTFontCreateFromUIFont(font);
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTFontAttributeName, theFont);
CFRelease(theFont);
CTParagraphStyleSetting settings[] = {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTParagraphStyleAttributeName, paragraphStyle);
CFRelease(paragraphStyle);
NSMutableAttributedString *ret = (NSMutableAttributedString *)attrString;
return [ret autorelease];
}
What about calculating the space needed to draw an attributable string? That's a little more involved, but it can be done. Here are two category methods on
NSAttributedStringthat will tell you how much space an attributed string will require when drawn at a specified width or height, which is a useful thing to know when laying out text:
NB(1): This is a new version that's both shorter, and fixes a bug with the original version.
NB(2): A couple of people on Twitter have commented that you should save a reference to your
CTFrameSetterRef when calculating height or width and re-use it, because the framesetter will cache those calculations. If you use a new one, you not only have the overhead of a new object, you will also be doing the size calculation twice. I'm planning a future post where I show how to draw attributed strings, and I need to give some thought about how to re-architect the code for that post based on that feedback.
- (CGFloat)boundingWidthForHeight:(CGFloat)inHeight
{
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) self);
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(CGFLOAT_MAX, inHeight), NULL);
CFRelease(framesetter);
return suggestedSize.width;
}
- (CGFloat)boundingHeightForWidth:(CGFloat)inWidth
{
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) self);
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(inWidth, CGFLOAT_MAX), NULL);
CFRelease(framesetter);
return suggestedSize.height;
}
I assume it's only a matter of time before Apple gives us the
NSAttributedString UIKit Additions category, or some similar higher-level functionality. In the meantime, any time you have to deal with attributed strings, the best bet is to figure out how to do what you need to do in Core Text and/or Core Graphics (Apple's Programming Guides actually show exactly how to do the most common tasks using both of these frameworks), then wrap a generic version of that code into a category method on
NSAttributedString or
NSMutableAttributableString.