Showing posts with label iOS SDK. Show all posts
Showing posts with label iOS SDK. Show all posts

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.

Monday, March 7, 2011

Design Then Code

Mike Rundle just put up a really nice beginner's tutorial on program iOS SDK applications from scratch. Although I disagree with him pretty violently about whether you should use Interface Builder (no, really, you should use it), it's otherwise a brilliant introduction; one of the best I've seen on the web for beginners.

Thursday, August 19, 2010

Transparent Grouped Tableviews

I ran into a problem today with a transparent grouped table view. On the table view, I set opaque to NO and set the background color to clearColor so that an image behind the table would show through:
Screen shot 2010-08-19 at 1.55.43 PM.png
I can't show you the actual app I was working on because I'm under NDA, but I've created a sample project that shows the problem using a really ugly background image. Notice the black around the corners of the group sections:
Screen shot 2010-08-19 at 1.54.21 PM.png

It took me a while to figure out why this was happening, but with a little crowd-source debugging thanks to Twitter, I've narrowed the culprit down. Even though you can clearly see in the first picture that the table view's background color has been set to clearColor in Interface Builder, that setting is either not being respected, or is being changed somewhere. Now, I'm using stock elements here - no custom views at all - so I know I'm not changing it.

My original fix for this was rather involved, but after a process of elimination where I kept commenting out bits of the fix and and seeing what impact it had, I was able to remove all of the code of my fix except for this line of code:

- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.backgroundColor = [UIColor clearColor];
}


Turns out all the other parts were red herrings leftover from my various attempts to fix the problem. So, if you're having this problem, just manually re-set the table view's background color to clearColor and you should be good.

(and yes, I'm going to file a bug report now)

Thursday, June 17, 2010

Grand Slam!

Apple has set a new record for WWDC video avialability this year, getting them out not even a week after the event ended. Amazing.

Even more amazing, they're not selling them this year. Registered developers can access them for free. Want to know what all the fuss is about with Xcode 4? Go watch the videos.

 
Design by Wordpress Theme | Bloggerized by Free Blogger Templates | coupon codes