Thursday, March 10, 2011

Attributed Strings in iOS

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.



12 comments:

Paul Warren said...

Jeff, I have a simple attributed label class in the iOS Recipes book with examples of using some of the attributed string components. The code for the book is free to check out on pragprog.com

Shawn said...

Paul, do you have a direct link to the example? I couldn't find anything on the pragprog.com website with respect to NSAttributedString

Paul said...

I think I've found it. The book is here:

http://pragprog.com/titles/cdirec/ios-recipes

The code examples are here:

http://pragprog.com/titles/cdirec/source_code

Paul said...

Further to that, I'm not on a machine with grep at the moment but there is a ref to NSAttributedString in the coreText folder.

Burnsoft Ltd said...

Good article here and git project.
http://www.cocoanetics.com/2011/01/uiwebview-must-die/

Pierre said...

I've always seen attributed strings in iOS as being only for use with CoreText (yes, that means you have an Objective-C API that's only useful when used with a C API… at least you can keep your Objective-C based attributed string building code). Even in the few places in Cocoa touch that take (or can take, on top of raw NSStrings) attributed strings, they are still CoreText-attributed strings since they have to be configured with CoreText attributes. CoreText is no more foreign to attributed strings than the attributed strings AppKit additions are: NSAttributedString simply defines a common infrastructure for any text engine to build upon (be it CoreText or AppKit text). There is no need for documentation to give the secret mapping between the kCTWhateverAttributeNames and the NSWhateverAttributeNames because there is none, the CoreText attributes are the native way to specify those; not a roundabout way for the NS ones you think you need.

Also, I wonder what is your need for measuring attributed strings prior to typesetting them: the intent is that you have CoreText typeset them in the space it will end up, then if it turns out there is not enough space, then you take action depending on that. That way you don't have to worry about caching your framesetters.

However, the situation with CTFont/UIFont is unfortunate; UIKit is usually good at not duplicating with different objects stuff already defined by CoreGraphics, for instance. Also, it's too bad you can't create attributed strings from styled text document formats (I must admit I have not noticed this, as my use of attributed string is for displaying subtitles/captions from a subtitle format I have to parse myself… joy).

My personal pet peeve is that CoreText does NOT support cutting with ellipses at the end of multiline text if it doesn't fit, as UILabel can do (in subtitles you don't have a second page or a scroll bar to contain the overflow). I have to apply crazy CoreText-fu to retypeset part of the string and get this behavior. rdar://problem/8818097

While I'm at it, I should also mention that on Mac OS X and iOS up until 4.3 (not tested the fix yet), CFAttributedStrings will crash if you apply an attribute to a range where this attribute already has that value (can happen for a number of reasons when building attributed strings). Be careful of this one, we didn't catch it in testing, it was reported to us in the wild. rdar://problem/8884848

Aman Seo said...

Hi,

Nice your blog and very nice your blog Ranking is too Good we like its you can see our also website for online shopping and plz see also

ノートパソコンのバッテリーノートパソコンのバッテリー 対応AC ラップトップアダプター ハードディスク キーボード HDD Dreambox タブレットは、 PCウルトラモバイルPCのPDA, GPS メモリ デスクトップ ノートパソコン画面 ウルトラモバイルPC、ネットブック MIDのバッテリー ビデオカメラバッテリー PDAのバッテリー 交換のデジタルカメラのバッテリー microSD/TransFlash 再充電式単3電池・単4電池 バッテリー充電器 zune ノートパソコンのバッテリー

adamwillsss said...

Nice post. Great information. Thanks for sharing it.

Slava said...

It'd really help to understand more if you put some screenshots with output on the iOS simulatore.

thanks.

James said...

The process of the style,looks and the designing plays an important part if you are in progress of developing some of the invention as you tell about the objects designed to hold strings along with font,and the setting with perfection is also important,so the details you tell in the form of example,it is really useful to see because this thing can help in order to make some tasks and usfeul inventions.
_______________________________
Dissertation

Ocean said...

real brilliant
as many are now opting for individual webmasters, electronics articles updates helps to know about those latest updates.
thanks for the post.

Harry said...

These pictures are so impressive. I am sure my peers will enjoy these too.childrens furniture