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.

Building for the MAS

If you have any thought of making the jump from iOS to the Mac App Store, bookmark this post by Craig Hockenberry right now. It's a brilliant and detailed guide to everything you need to do to get your application on the Mac App Store from somebody who's been there.

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.

Friday, March 4, 2011

The iPad 2 Rant

MartianCraft has a fair amount of Android work right now. Personally, I try to focus on the iOS work whenever possible, but we're a small company, so nobody gets to play the primadonna. As a result, I spend a good chunk of my time on Android projects and have to stay abreast of both the Android and iOS worlds from both a hardware and a software perspective.

Last week, I found myself grudgingly admitting to myself that the Motorola Xoom is not a bad tablet. It feels incomplete in many ways. It has rough edges, some definite hardware and software CBBs¹, and a general dearth of good, native-resolution apps. But, it had potential and I definitely saw how certain demographics might be attracted to it over the iPad. I saw a tablet that normal people could use with some frustration, but not an insurmountable amount… much like Windows, post Windows-95.

The thing that I haven't seen in any of the Android tablets, however, is a compelling reason to buy them instead of the iPad. The only people I know who've bought Android tablets also own iPads. Other than a strong aversion to Apple's products or Apple as a company, what would compel somebody to pay $800 for a Xoom or Tab rather than going out and getting an iPad? Maybe there's a reason, but I can't see it. While both the Xoom and Tab had some specs that were better than the original iPad, neither offered a comparable experience let alone a better one, and neither could do anything that the iPad can't², despite higher price tags.

Then came yesterday.

Two days ago, the Xoom looked like a decent, almost finished and slightly overpriced tablet. Two days ago, it had a couple of quantifiable advantages, including native CDMA support and a better GPU. Two days ago, you could make the Xoom look better than the iPad on paper. Though marketing based on tech specs hasn't proven to be a very effective strategy in mobile computing space, at least they did have that for them. They had grounds for claiming you should buy the Xoom instead of an iPad. The arguments were thin, but two days ago they existed.

Today, simply put: The Xoom is fucked. So, I suspect, is the unreleased Samsung Tab 10.1 and the RIM Playbook. I can only imagine the discussions that are going on inside those companies today.

Only the staunchest Apple haters and self-deluded "openness" ideologues are going to pony up that kind of dough for a tablet that can't offer a comparable experience and doesn't have better tech specs. The Xoom doesn't even have the advantage of working with a carrier that Apple's tablet doesn't. In seven days, there will be both native CDMA and GSM models of the iPad 2.

Think about this: yesterday when I checked, the Android Marketplace had sixteen Honeycomb tablet-resolution apps. Sixteen. And you know what's not included in that sixteen? That space game that they show the guy playing in the Xoom commercials. In other words, they had to put a fake game in the commercial. Would they have done that if they had even one compelling application that could make the Xoom look better than the iPad?

As a tablet platform, Android has two big challenges.

First, it has a chicken-and-egg problem with software. Developers are waiting for people to buy Android tablets in sufficient quantity to support the platform, and many consumers are waiting for good apps to buy Android. In the phone world, Android seems to be past that hump. While the app situation is nowhere near as good as on iOS yet, there are apps — including some good ones — for the platform.

But, even if the Xoom were every bit as amazing of a piece of hardware as the iPad 2, it would still have the problem that it does less cool things. There's nothing comparable to Garage Band or iMovies, or any of the hundreds of jaw-dropping iPad apps that have been created in the last year like Infinity Blade, The Elements, or Alice. There's just no "wow" app you can put on your Xoom and show people that's going to make them want to run out and buy one. There's nothing you can do and confidently say "your iPad can't do that shit right there, bitch".

The second, and much larger problem is simply one of price. I see people constantly comparing the Android/iOS situation to the Windows/Mac situation of the eighties and nineties. I usually see this claim by people laughably arguing that Apple's failure is imminent.

In the nineties, Apple kept insane profit margins on their products while dozens of manufacturers created inexpensive commodity PCs running Windows. There was a margin war on the PC side, and PCs became noticeably cheaper (despite paying hefty licensing fees to Microsoft), and that price difference, combined with Microsoft closing some of the usability gap with the Mac, is what lead to the dominance of Wintel machines. In the nineties, Macs simply cost more. You could argue that Macs were cheaper based on TOC or employee efficiency, but in the quantifiable terms that bean counters understand, the Mac was a lot more expensive and didn't do noticeably more, especially once Adobe jumped ship and become cross-platform.

That's not where things are now, however. For typical consumers - people who don't have a dog in the technology race, so to speak, are going to buy based largely on price, Apple's mobile "post-PC devices" aren't just better than their competitors, they're cheaper than comparable competitors.

We don't have a situation where commodity resellers can easily assemble components into a working, desirable mobile device. Mobile devices are all about form factor, design, and ease of use. They don't sit on a desk, they go where you go. They need to be well engineered, light, get good battery life, and be easy to use. They can't require IT support staff, an instruction manual, or training. A large beige box on a desk is one thing, but in your pocket it's another thing altogether.

Why is this the case, though? Why can't these companies compete with Apple on price in the tablet space?

The prices Apple can offer is a result of two things. First, is plain and simple buying power. Apple sells a lot of devices, so they buy a lot of screens, flash memory, etc. As a result, they can get quantity discounts. Apple got to the 10 inch form factor first and cornered the market, driving up the price for 10" screen components for any competitors coming after them.

The second, however, is that they have gobs of cash on hand. A lot of market watchers say Apple is foolhardy to keep so much cash on hand. On the contrary! Apple understands psychology, and not just consumer psychology. When they go to a vendor or hardware partner and ask for exclusive arrangements, priority fulfillment, or better prices, do you know what bargaining chip they have that few other companies have?

The corporate equivalent of a suitcase full of cash.

When a vendor needs to retool for a new manufacturing process Apple has developed or needs to increase their output capacity, Apple shows up with a wad of cash in hand. They don't have to liquidate any assets or get a loan or seek permission of shareholders. They just play Daddy Warbucks and pull out a wad of million dollar bills. Apple's partners, in addition to getting large-volume contracts, can get working capital as part of their arrangment with Apple without taking out loans. A definite part of the reason you were able to buy an iPad for only $499 is because Apple didn't follow conventional wisdom about cash on hand.

Arguing that Apple would be doing better by doing what everybody else is doing isn't usually very convincing to me.

Motorola and Samsung… they're both large companies with a lot of buying power and strong brand recognition. The problem is, they don't understand the game that Apple's playing in the mobile space, so they're playing it wrong. They're so caught up in catching up that they're not even trying to innovate in this space. Maybe HP or Rim will figure it out, but I'm not going to hold my breath.

Which is unfortunate. If Apple's doing this kind of amazing stuff without any viable competition, can you imagine what they'd be doing with strong, viable competitors nipping at their heels?


1 "Could Be Betters"
2 From a consumer perspective, not from the perspective of a geek who likes to take things apart and put them back together. The Xoom, with its more powerful processor and GPU had the potential to do things the original iPad couldn't, but didn't ship with any application that proved it. Consumers believe what they see, not what the tech specs say.

Thursday, February 24, 2011

QuickBoot

If you're going to install the 10.7 preview on a separate hard drive or partition, it's definitely worth knowing about Buttered Cat Software's QuickBoot, which allows you to change startup disks and reboot from your menu bar.

Lion in the House

Today, Apple released a preview version of Mac OS X Lion, but only for registered Mac developers. This is a big release, not just in terms of front-end experience, but also under the hood. A lot of the changes are influenced by UIKit and iOS, hence the moniker "Back to the Mac", but there's also a fair amount of completely new goodness, some of which will likely roll down to iOS at some point.

If you're an iOS developer, but not a Mac developer, it's probably worth $99 to get access to Tiger Lion. It'll give you a better idea of where Apple is taking things.

Plus, there's just a lot of really cool stuff in there.

Interestingly, the OS preview is being delivered via the Mac App Store, which might be a hint at how paid OS upgrades will be handled in the future.

Members of the Mac developer program can log into the Mac Developer site to download Lion, the release notes, and a new version of Xcode 4.

Wednesday, February 23, 2011

Voices that Matter Seattle

So far, 2011 has been pretty light for me in terms of speaking. I do, however, have one speaking engagement lined up for this year: I'll be speaking at Voices that Matter in the land of Mordor Seattle on April 9th and 10th. If you're interested in going, early bird pricing ends on February 25.

You can use the speaker code SEASPK2 to get $100 off.

There's a great speaker lineup for this conference, including Andy Ihnatko.