<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss2full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" version="2.0">

<channel>
	<title>fill the void</title>
	
	<link>http://www.bdunagan.com</link>
	<description />
	<lastBuildDate>Sun, 08 Nov 2009 21:48:54 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.8.4</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" href="http://feeds.feedburner.com/FillTheVoid" type="application/rss+xml" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com" /><item>
		<title>iPhone Tip: Conditional Toolbar</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/_HywSgNojyU/</link>
		<comments>http://www.bdunagan.com/2009/11/08/iphone-tip-conditional-toolbar/#comments</comments>
		<pubDate>Sun, 08 Nov 2009 21:48:54 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=408</guid>
		<description><![CDATA[
For an iPhone app I&#8217;m developing, I wanted a toolbar at the bottom of the initial view, but I didn&#8217;t want it appearing in the other views. I tried simply calling myUINavigationController.hidden, but the effect was a little jarring. UINavigationController has a great wrapper for that method that includes animation: setToolbarHidden:animated:. With animation, the toolbar [...]]]></description>
			<content:encoded><![CDATA[<p>
For an iPhone app I&#8217;m developing, I wanted a toolbar at the bottom of the initial view, but I didn&#8217;t want it appearing in the other views. I tried simply calling <tt>myUINavigationController.hidden</tt>, but the effect was a little jarring. <a href="http://developer.apple.com/IPhone/library/documentation/UIKit/Reference/UINavigationController_Class/Reference/Reference.html"><tt>UINavigationController</tt></a> has a great wrapper for that method that includes animation: <tt>setToolbarHidden:animated:</tt>. With animation, the toolbar smoothly slides in and out when going between the initial view and the other views. I inserted the method in <a href="http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html"><tt>UIViewController</tt>&#8217;s</a> <tt>viewWillAppear</tt> and <tt>viewWillDisappear</tt>.
</p>
<pre class="brush: cpp;">
// in a UIViewController subclass

- (void)viewWillAppear:(BOOL)animated {
	[self.navigationController setToolbarHidden:NO animated:YES];
    [super viewWillAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated {
	[self.navigationController setToolbarHidden:YES animated:YES];
	[super viewWillDisappear:animated];
}
</pre>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/_HywSgNojyU" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/11/08/iphone-tip-conditional-toolbar/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/11/08/iphone-tip-conditional-toolbar/</feedburner:origLink></item>
		<item>
		<title>iPhone Tip: Automatic Focus for Text Input</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/ivFeXeB1UQc/</link>
		<comments>http://www.bdunagan.com/2009/11/07/iphone-tip-automatic-focus-for-text-input/#comments</comments>
		<pubDate>Sat, 07 Nov 2009 20:27:23 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=399</guid>
		<description><![CDATA[
Google popularized this trend in web forms years ago. When I go to the Google homepage, the page automatically puts the cursor in the search text box, so that I can immediately start typing. In Things on the iPhone, when I start to add a new item, the &#8220;New To Do&#8221; page automatically selects the [...]]]></description>
			<content:encoded><![CDATA[<p>
Google popularized this trend in web forms years ago. When I go to the Google homepage, the page automatically puts the cursor in the search text box, so that I can immediately start typing. In <a href="http://itunes.apple.com/us/app/things/id284971781?mt=8">Things</a> on the iPhone, when I start to add a new item, the &#8220;New To Do&#8221; page automatically selects the Title text field. Again, I can immediately start typing. This autofocus is very easy to achieve: simply call <tt>[myUITextField becomeFirstResponder]</tt>. Changing the keyboard style is equally trivial: <tt>myUITextField.keyboardType = UIKeyboardTypeEmailAddress</tt>. Both are attributes of the <a href="http://developer.apple.com/iphone/library/documentation/uikit/reference/UITextInputTraits_Protocol/Reference/UITextInputTraits.html#//apple_ref/occ/intf/UITextInputTraits">UITextInputTraits protocol</a>.
</p>
<pre class="brush: cpp;">
// Automatically highlight the field and change to email keyboard.
UITableViewCell *cell = (UITableViewCell *)[tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
[cell.field becomeFirstResponder];
cell.field.keyboardType = UIKeyboardTypeEmailAddress;
</pre>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/ivFeXeB1UQc" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/11/07/iphone-tip-automatic-focus-for-text-input/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/11/07/iphone-tip-automatic-focus-for-text-input/</feedburner:origLink></item>
		<item>
		<title>Design Iteration</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/bqR7rsZi_4U/</link>
		<comments>http://www.bdunagan.com/2009/10/01/design-iteration/#comments</comments>
		<pubDate>Fri, 02 Oct 2009 06:55:17 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[design]]></category>
		<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=382</guid>
		<description><![CDATA[
The feature I alluded to in my &#8220;Rereading Info.plist&#8221; post is a logging preference. It has two requirements: user-configurable (i.e. not code) but not immediately visible. When a user starts having technical issues and calls tech support, they find out how to turn on logging, and with that output, development has a better idea of [...]]]></description>
			<content:encoded><![CDATA[<p>
The feature I alluded to in my <a href="http://www.bdunagan.com/2009/09/30/rereading-info-plist/">&#8220;Rereading Info.plist&#8221; post</a> is a logging preference. It has two requirements: user-configurable (i.e. not code) but not immediately visible. When a user starts having technical issues and calls tech support, they find out how to turn on logging, and with that output, development has a better idea of what the issues are.
</p>
<p>
A year ago, when I first implemented it, I was averse to adding a secret preferences window, because I couldn&#8217;t think of a natural place for accessing it. Instead, I added a quick key-value pair to Info.plist. Users wouldn&#8217;t happen across it; most of them probably don&#8217;t know app bundles are folders. But the preference was there if needed. So, I shipped that. Multiple times. Recently, I decided this preference needed to be dynamic, for those times when a user wants to enable logging without restarting the application. I set up an <tt>NSTimer</tt> to read the key-value pair from Info.plist every minute. After finding out the dictionary was cached, I figured out how to manually read it. Problem solved.
</p>
<p>
And yet, I completely ignored all the signs that pointed to a bad user experience. The workflow for setting the preference was complicated to explain to other developers (who know app bundles are folders). Non-developers asked for the specific steps multiple times. And yes, the Info.plist dictionary is <i>cached</i>, implying it&#8217;s for general application information, not debug settings. Still, it took two thoughtful comments on my other post to snap me out of my developer haze and think about the feature from a user&#8217;s perspective.
</p>
<p>
The goal of the feature was to hide the user-configurable preference from immediate view, not to bury it completely. The Info.plist workflow didn&#8217;t work, so I started brainstorming. How about a secret preference window? Fine, but where would it go? Perhaps an <tt>NSMenuItem</tt>, but that&#8217;s not hidden. Then I realized an alternate <tt>NSMenuItem</tt> was the perfect fit: hold the option key and &#8220;Preferences&#8230;&#8221; switches to &#8220;Secret Preferences&#8230;&#8221;. A user won&#8217;t stumble across it, but the menu item is a key away. Moreover, an <tt>NSTimer</tt> isn&#8217;t necessary; Cocoa Bindings make a preference change instant. That&#8217;s what I implemented today.
</p>
<p>
Never be afraid to iterate your design. The product I work on went through <i>many</i> iterations before arriving at its current form. Apple agrees. When I&#8217;ve talked to their UI evangelists, John Geleynse and Eric Hope, both consistently cite design iteration as one of the most important processes for developing award-winning applications (along with <a href="http://developer.apple.com/iphone/library/documentation/UserExperience/Conceptual/MobileHIG/DesigningNativeApp/DesigningNativeApp.html#//apple_ref/doc/uid/TP40006556-CH4-SW2">product definition statements</a>). Most apps start out looking mediocre; the best user experiences are the culmination of many iterations. Keep iterating.</p>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/bqR7rsZi_4U" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/10/01/design-iteration/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/10/01/design-iteration/</feedburner:origLink></item>
		<item>
		<title>Rereading Info.plist</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/ShNnbgtnfAE/</link>
		<comments>http://www.bdunagan.com/2009/09/30/rereading-info-plist/#comments</comments>
		<pubDate>Thu, 01 Oct 2009 04:38:47 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=372</guid>
		<description><![CDATA[
Apple has a nice mechanism for storing important app bundle information outside of the application executable: Info.plist. All the applications in /Applications have one; just right-click on any app, select &#8220;Show Package Contents&#8221;, then click on &#8220;Contents&#8221;. The file elegantly houses many facts (version, name, copyright) about your app. And Apple has a straight-forward API [...]]]></description>
			<content:encoded><![CDATA[<p>
Apple has a nice mechanism for storing important app bundle information outside of the application executable: Info.plist. All the applications in /Applications have one; just right-click on any app, select &#8220;Show Package Contents&#8221;, then click on &#8220;Contents&#8221;. The file elegantly houses many facts (version, name, copyright) about your app. And Apple has a straight-forward API to get at that data: <tt>[[NSBundle mainBundle] infoDictionary]</tt>. The call returns an <tt>NSDictionary</tt> filled with key-value pairs from the Info.plist file. This workflow allows me to easily add my own key-value pairs to the file and quickly access them in code.
</p>
<p>
Recently, I wanted to read the key-value pair <i>dynamically</i>. That way, the user could change the value at any time, and within a minute, the app would use the new value. This proved harder. As it turns out, Apple optimized the <tt>infoDictionary</tt> query, so that it caches the contents at startup and returns that cached version through the application lifetime. If the user modified the key-value pair, that change wouldn&#8217;t show up until the next startup. Instead of using that builtin method, I had to read the Info.plist file directly. See the code below.
</p>
<pre class="brush: cpp;">
// [[NSBundle mainBundle] infoDictionary] is cached at app startup, so we need to read the file directly for latest data.
NSString *infoPlistFilePath = [NSString stringWithFormat:@&quot;%@/Contents/Info.plist&quot;, [[NSBundle mainBundle] bundlePath]];
NSString *infoDictionary = [NSDictionary dictionaryWithContentsOfFile:infoPlistFilePath];
NSString *value = [infoDictionary valueForKey:@&quot;InfoPlistKey&quot;];
</pre>
<p>
<b>UPDATE</b>: This is a lesson in design. While the code does function as described, I now agree that no user should be subjected to this workflow. Check out my <a href="http://www.bdunagan.com/2009/10/01/design-iteration/">longer explanation about the feature&#8217;s design</a>.</p>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/ShNnbgtnfAE" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/09/30/rereading-info-plist/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/09/30/rereading-info-plist/</feedburner:origLink></item>
		<item>
		<title>Syncing Arrows in an iPhone App</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/bWQmIe9pSPE/</link>
		<comments>http://www.bdunagan.com/2009/09/29/syncing-arrows-in-an-iphone-app/#comments</comments>
		<pubDate>Wed, 30 Sep 2009 04:48:49 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=352</guid>
		<description><![CDATA[
My last post illustrated how to add syncing arrows to a Mac application, like iPhoto and iTunes have. But what adding that user feedback to an iPhone app? Don&#8217;t worry; it&#8217;s even easier, thanks to Core Animation. In fact, there are multiple paths to those rotating arrows: an image array and a simple rotation transform.


In [...]]]></description>
			<content:encoded><![CDATA[<p>
My last post illustrated how to <a href="http://www.bdunagan.com/2009/09/25/syncing-arrows-in-itunes/">add syncing arrows to a Mac application</a>, like iPhoto and iTunes have. But what adding that user feedback to an iPhone app? Don&#8217;t worry; it&#8217;s even easier, thanks to Core Animation. In fact, there are multiple paths to those rotating arrows: an image array and a simple rotation transform.
</p>
<p>
In both cases, we&#8217;ll use a custom <tt>UITableViewCell</tt> to drive the animation, unlike the model-driven approach we used on the Mac. See my <a href="http://www.bdunagan.com/2009/06/28/custom-uitableviewcell-from-a-xib-in-interface-builder/"><tt>custom UITableViewCell</tt> post</a> for a tutorial.
</p>
<p><img src="/files/SyncingArrows_iPhone.png"/></p>
<h3>Animate using image array</h3>
<p>
<tt>UIImageView</tt> has three properties available to facilitate animations: <tt>animationDuration</tt>, <tt>animationImages</tt>, and <tt>animationRepeatCount</tt>. We can use these to easily incorporate the six iPhoto arrow images (see the <a href="http://www.bdunagan.com/2009/09/25/syncing-arrows-in-itunes/">Mac syncing arrows post</a>) into our custom <tt>UITableViewCell</tt>. For indefinite animation (which we want), set <tt>animationRepeatCount</tt> to zero, according to <a href="http://developer.apple.com/iPhone/library/documentation/UIKit/Reference/UIImageView_Class/Reference/Reference.html#//apple_ref/occ/instp/UIImageView/animationRepeatCount">Apple&#8217;s documentation</a>. Then, after setting the duration, we simply start the animation.
</p>
<pre class="brush: cpp;">
// Animate using image array.
syncIconView.animationImages = [NSArray arrayWithObjects:
    [[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows1&quot; ofType:@&quot;png&quot;]] autorelease],
    [[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows2&quot; ofType:@&quot;png&quot;]] autorelease],
    [[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows3&quot; ofType:@&quot;png&quot;]] autorelease],
    [[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows4&quot; ofType:@&quot;png&quot;]] autorelease],
    [[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows5&quot; ofType:@&quot;png&quot;]] autorelease],
    [[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows6&quot; ofType:@&quot;png&quot;]] autorelease],
    nil];
syncIconView.animationDuration = interval;
syncIconView.animationRepeatCount = 0;
[syncIconView startAnimating];
</pre>
<h3>Animate using rotation transform</h3>
<p>
Instead of leveraging <tt>UIImageView</tt>-specific methods, we utilize the iPhone&#8217;s layer-backed <tt>UIViews</tt> and create a <tt>CAAnimation</tt>, with a rotation transform. To match the array animation, we make the <tt>toValue</tt> equal to pi radians (180 degrees). Unlike the array animation, we cannot supply a specific argument to have the animation repeat indefinitely, so we just assign a really large value. (Let me know if there is a better workaround.) Finally, we add the new animation to the <tt>UIImageView</tt>&#8217;s layer.
</p>
<pre class="brush: cpp;">
// Animate using rotate transform.
CABasicAnimation* rotateAnimation = [CABasicAnimation animationWithKeyPath:@&quot;transform.rotation&quot;];
rotateAnimation.toValue = [NSNumber numberWithFloat:M_PI];
rotateAnimation.duration = interval;
rotateAnimation.repeatCount = 1e100;
[syncIconView.layer addAnimation:rotateAnimation forKey:@&quot;rotateAnimation&quot;];
</pre>
<h3>Results</h3>
<p>
The difference between the two animations is quite striking. At a small duration (around .3s), the array animation looks fine without comparison. The animation is almost imperceptibly jumpy but is, again, just fine. However, a side-by-side comparison of it to the rotation transform animation reveals the stark contrast between the two approaches. The latter shows how amazing Core Animation makes the UI; it&#8217;s magic pixie dust. I committed a sample project (10.5, Xcode 3.1.4) to <a href="http://code.google.com/p/bdunagan/">Google Code</a>. Check it out in SVN, run it, and compare &#8220;Array at 1.00s&#8221; with &#8220;Rotate at 1.00s&#8221;. The rotate transform wins. As does Core Animation.</p>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/bWQmIe9pSPE" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/09/29/syncing-arrows-in-an-iphone-app/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/09/29/syncing-arrows-in-an-iphone-app/</feedburner:origLink></item>
		<item>
		<title>Syncing Arrows in iTunes</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/eR4zfYNbHas/</link>
		<comments>http://www.bdunagan.com/2009/09/25/syncing-arrows-in-itunes/#comments</comments>
		<pubDate>Sat, 26 Sep 2009 05:54:05 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=337</guid>
		<description><![CDATA[
When I sync in my iPhone, I love the syncing arrows in the source list. They smoothly turn around and around, letting me know that iTunes is busy syncing all of my data. iPhoto shows the same rotating arrows when it syncs a MobileMe album. Unfortunately, those arrows aren&#8217;t an NSProgressIndicator Cocoa object; those objects [...]]]></description>
			<content:encoded><![CDATA[<p>
When I sync in my iPhone, I love the syncing arrows in the source list. They smoothly turn around and around, letting me know that iTunes is busy syncing all of my data. iPhoto shows the same rotating arrows when it syncs a MobileMe album. Unfortunately, those arrows aren&#8217;t an <tt>NSProgressIndicator</tt> Cocoa object; those objects only display the spinning lines for <tt>isIndeterminate:YES</tt> (see my <a href="http://www.bdunagan.com/2008/12/06/nsprogressindicator-in-nstableview-or-nsoutlineview/"><tt>NSProgressIndicator</tt> example</a> from last year). Instead, that nice UI feedback is created through home-brewed animation.
</p>
<p>
On an side note, comparing the Resources folders in iTunes and iPhoto gives some insight into Apple&#8217;s approach with each product. (Right-click on any application and click &#8220;Show Package Contents&#8221; to navigate to the Resources folder.) iTunes&#8217; Resources folder holds a mere 118 items, including 19 language folders. Only 118! There are extremely few icons in the folder, implying that iTunes draws almost all of them in code. On the other hand, iPhoto has 1,532 items in its Resources folder. Seemingly every state of every icon in the entire photo application comes from an icon in this folder. Different approaches. The end result is iTunes weighing in at 150MB and iPhoto at 410MB. Perhaps the stark differences are market-driven, as iTunes is predominately delivered online whereas iPhoto is typically pre-installed or comes on a DVD. For this tutorial, I&#8217;m going with iPhoto&#8217;s approach.
</p>
<p>
Let&#8217;s make syncing arrows for a sample Mac application. Originally, I thought I should use Core Animation, as it&#8217;s in 10.5+. However, at WWDC, an Apple Core Animation engineer told me that only the iPhone apps can rotate table cells like that, as only <tt>UITableViewCells</tt> subclass a view object (<tt>UIView</tt>). On the Mac, <tt>NSCell</tt> subclasses <tt>NSObject</tt> directly, so he said to go with a simple NSTimer and cycle through a set of images. So, that&#8217;s what I did here.
</p>
<p><img src="/files/SyncingArrows.png"/></p>
<p>
I want a simple application for this demo: just a list (<tt>NSTableView</tt>). Inside each row, I want those syncing arrows, rotating at varying speeds to see which interval is the optimal. There are two parts to this UI: an NSTimer and a set of icons in my cell class. To make the code very simply, I leverage Cocoa Bindings to trigger a UI refresh, instead of calling <tt>NSView::setNeedsDisplay</tt>. The screenshot below shows how to connect the <tt>NSArrayController</tt> in Interface Builder to the two <tt>NSTableColumns</tt> in the <tt>NSTableView</tt>.
</p>
<p><img src="/files/SyncingArrows_IB.png"/></p>
<p>
The icons are straight out of iPhoto. Go into iPhoto.app/Contents/Resources and search for &#8220;sl-status_syncanimation-01.tiff&#8221;. There are six total.
</p>
<p><img src="/files/iphoto_syncingarrows.png"/></p>
<p>
First, I initialize the cell&#8217;s state, including the timer and the icon array.
</p>
<pre class="brush: cpp;">
- (id) initWithInterval:(NSTimeInterval)interval {
	self = [super init];
	if (self != nil) {
		// Set item's title.
		self.title = [NSString stringWithFormat:@&quot;Item with interval %1.2fs&quot;, interval];
		// Set up status array.
		icons = [[NSArray alloc] initWithObjects:
				 [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows1&quot; ofType:@&quot;png&quot;]],
				 [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows2&quot; ofType:@&quot;png&quot;]],
				 [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows3&quot; ofType:@&quot;png&quot;]],
				 [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows4&quot; ofType:@&quot;png&quot;]],
				 [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows5&quot; ofType:@&quot;png&quot;]],
				 [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@&quot;syncing_arrows6&quot; ofType:@&quot;png&quot;]],
				 nil];
		// Initialize icon state.
		icon = [icons objectAtIndex:0];
		statusState = 0;
		// Set up status timer.
		stateTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(updateStatus) userInfo:nil repeats:YES];
	}
	return self;
}
</pre>
<p>
Next, I simply add the method called by the <tt>NSTimer</tt>. In there, I update the status icon and increment to the next state.
</p>
<pre class="brush: cpp;">
- (void)updateStatus {
	self.icon = [icons objectAtIndex:statusState];
	statusState = (statusState + 1) % [icons count];
}
</pre>
<p>
I posted the code on Google Code as <a href="http://code.google.com/p/bdunagan/">SyncingArrows</a>. Keep in mind that I wrote it in Leopard (10.5) using Xcode 3.1.2; let me know if you have issues trying it.</p>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/eR4zfYNbHas" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/09/25/syncing-arrows-in-itunes/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/09/25/syncing-arrows-in-itunes/</feedburner:origLink></item>
		<item>
		<title>Icons, Iterated</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/Onb8cjwIL7g/</link>
		<comments>http://www.bdunagan.com/2009/09/24/icons-iterated/#comments</comments>
		<pubDate>Fri, 25 Sep 2009 05:58:05 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[random]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=326</guid>
		<description><![CDATA[
iStockPhoto is awesome. I updated the blog icons just now with a great set from iStockPhoto: Satin Icons from scottdunlap. Very vibrant and clean. I love all of his icons. I couldn&#8217;t find a LinkedIn icon on the site, so I googled around and came across IconsPedia. A user there, Quaqe9, posted a huge set [...]]]></description>
			<content:encoded><![CDATA[<p>
<a href="http://www.istockphoto">iStockPhoto</a> is awesome. I updated the blog icons just now with a great set from iStockPhoto: <a href="http://www.istockphoto.com/stock-illustration-9834110-satin-icons-blog-social-media.php">Satin Icons</a> from <a href="http://www.istockphoto.com/user_view.php?id=245060">scottdunlap</a>. Very vibrant and clean. I love all of his icons. I couldn&#8217;t find a LinkedIn icon on the site, so I googled around and came across <a href="http://www.iconspedia.com">IconsPedia</a>. A user there, Quaqe9, posted a huge set of social media icons, <a href="http://www.iconspedia.com/icon/linkedin-4221.html">LinkedIn</a> among them. If you&#8217;re at all dissatisfied with your icons, definitely check out iStockPhoto; there is a lot of fantastic work there, but pay attention to the license agreement if it&#8217;s for a commercial product.
</p>
<p>
I&#8217;m pretty happy with the iteration.
</p>
<p><img src="/files/icons_iterated.png"/></p>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/Onb8cjwIL7g" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/09/24/icons-iterated/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/09/24/icons-iterated/</feedburner:origLink></item>
		<item>
		<title>ibtool scripts on Google Code</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/nHcao0KDC2Y/</link>
		<comments>http://www.bdunagan.com/2009/08/10/ibtool-scripts-on-google-code/#comments</comments>
		<pubDate>Tue, 11 Aug 2009 06:31:29 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=319</guid>
		<description><![CDATA[
I added the four localization scripts from previous posts to Google Code. In addition, I added a readme to document how to use them, as they do require some setup to work smoothly. Below is the readme and the script for incremental localization.

readme

=Localization Readme=
This readme covers four scripts I wrote to utilize ibtool. They're all [...]]]></description>
			<content:encoded><![CDATA[<p>
I added the four localization scripts <a href="http://www.bdunagan.com/2009/03/15/ibtool-localization-made-easy/">from</a> <a href="http://www.bdunagan.com/2009/04/15/ibtool-localization-made-easier/">previous</a> <a href="http://www.bdunagan.com/2009/06/27/ibtool-caveats/">posts</a> to <a href="http://code.google.com/p/bdunagan/source/browse/#svn/trunk/Localization">Google Code</a>. In addition, I added a readme to document how to use them, as they do require some setup to work smoothly. Below is the readme and the script for incremental localization.
</p>
<p><b>readme</b><br />
<tt><br />
=Localization Readme=<br />
This readme covers four scripts I wrote to utilize ibtool. They're all available on Google Code with MIT license.<br />
- Brian (brian@bdunagan.com)</p>
<p>These first two scripts don't require a complicated environment, just a directory with the applicable contents.</p>
<p>====generate_xib_strings.rb====<br />
http://www.bdunagan.com/2009/03/15/ibtool-localization-made-easy/<br />
Usage: ruby generate_xib_strings.rb path_to_lproj<br />
Point the script at the folder containing the XIBs and it will generate .strings files for each of them, titled xib_name.strings.</p>
<p>====validate_strings_files.rb====<br />
http://www.bdunagan.com/2009/06/27/ibtool-caveats/<br />
Usage: ruby validate_strings_files.rb path_to_strings<br />
Point the script at the folder containing the .strings and it will validate them, beyond what ibtool (build 677, v3.1.2) catches. (ibtool silently fails on many characters.)</p>
<p>These second two scripts do require a complicated environment. They both take localized .strings files and incorporate them into localized .lproj folders, either by clobbering or updating. Both scripts expect the .strings to be laid out like below. Both scripts depend on the .strings folders to match the .lproj folder names.</p>
<p>+ strings_folder/<br />
--+ DE/<br />
----+ AnotherWindow.strings<br />
----+ Localizable.strings<br />
----+ MainWindow.strings<br />
--+ ES/<br />
----+ AnotherWindow.strings<br />
----+ Localizable.strings<br />
----+ MainWindow.strings<br />
--+ FR/<br />
----+ AnotherWindow.strings<br />
----+ Localizable.strings<br />
----+ MainWindow.strings</p>
<p>====localize_xibs.rb====<br />
http://www.bdunagan.com/2009/03/15/ibtool-localization-made-easy/<br />
Usage: ruby localize_xibs.rb path_to_project path_to_strings<br />
Creating a localized XIB is remarkably easy. A simple call to ibtool handles it, given a localized .strings file and an English version of the XIB. Keep in mind this script will clobber the existing set of localized XIBs.</p>
<p>====localize_xibs_incremental.rb====<br />
http://www.bdunagan.com/2009/04/15/ibtool-localization-made-easier/<br />
Usage: ruby localize_xibs_incremental.rb path_to_project path_to_strings<br />
This is the most complicated script, but it shows how powerful ibtool is. It incrementally updates localized XIBs. Apple goes into great detail on its documentation: http://developer.apple.com/documentation/developertools/conceptual/IB_UserGuide/LocalizingNibFiles/LocalizingNibFiles.html#//apple_ref/doc/uid/TP40005344-CH13-SW8. Essentially, ibtool will diff v1 and v2 of English.lproj/MainWindow.xib and stick those changes along with new localized strings into v2 of fr.lproj/MainWindow.xib while preserving changes from v1 of fr.lproj/MainWindow.xib. Resizing UI elements is a perfect example of changes that are preserved in incremental updates. It does require even more preprocessing as a setup. In addition to the strings folder directory structure, all the relevant .lproj folders need to be laid out in a specific manner.</p>
<p>+ English.lproj/<br />
--+ AnotherWindow.new.xib<br />
--+ AnotherWindow.old.xib<br />
--+ Localizable.strings<br />
--+ MainWindow.new.xib<br />
--+ MainWindow.old.xib</p>
<p>+ fr.lproj/<br />
--+ AnotherWindow.old.xib<br />
--+ Localizable.strings<br />
--+ MainWindow.old.xib</p>
<p>I wrote the script in terms of xib_name.old.xib and xib_name.new.xib. The old version of the localized XIB is simply the existing version, as I want to create the new version. I do name the new version xib_name.xib intentionally so that it's ready to be committed to the source code repository, presumably where it already exists. The new version of the English XIB is the latest version also, but the old version of the English XIB is the checkpoint in the revision history where I last updated the localized XIB.</p>
<p>Finally, the last part of the script's workflow actually generates .strings files for the old and new versions of the localized XIB. I always diff this pair of files to ensure the update process went smoothly.<br />
</tt></p>
<p><b>localize_xibs_incremental.rb</b></p>
<pre class="brush: cpp;">
#!/usr/bin/ruby
# MIT license

# localize_xibs_incremental.rb
# This script takes a set of localized strings, a set of old and new English XIBs, and a set of current
# localized XIBs and incrementally localizes the localized XIBs with the localized strings and the diff between
# the old English XIBs and the new English XIBs.

# ibtool
#   --previous-file English.lproj/MainWindow.old.xib
#   --incremental-file fr.lproj/MainWindow.old.xib
#   --strings-file path_to_strings/fr/MainWindow.strings
#   --localize-incremental
#   --write fr.lproj/MainWindow.xib
#   English.lproj/MainWindow.new.xib

# More info: http://developer.apple.com/documentation/developertools/conceptual/IB_UserGuide/LocalizingNibFiles/LocalizingNibFiles.html

require 'FileUtils'

# Check for arguments.
if ARGV.length != 2
  puts &quot;Usage: ruby localize_xibs_incremental.rb path_to_project path_to_strings&quot;
  exit
end

# Get path arguments and 'cd' to that project path.
SOURCE_XIB_FOLDER = &quot;English.lproj&quot;
PROJECT_PATH = ARGV[0]
STRINGS_PATH = ARGV[1]
FileUtils.cd(PROJECT_PATH)
FILES_TO_IGNORE = [&quot;.svn&quot;, &quot;.&quot; ,&quot;..&quot;, &quot;.DS_Store&quot;]
NON_XIB_LOCALIZED_FILES = [&quot;InfoPlist.strings&quot;,&quot;Localizable.strings&quot;]

# Iterate through the current directory.
Dir.entries(STRINGS_PATH).each do |localized_folder|
  if (!FILES_TO_IGNORE.include?(localized_folder))
    puts &quot;Generating localizations for #{localized_folder}&quot;
    # Iterate over the .strings language folders.
    Dir.entries(STRINGS_PATH + &quot;/&quot; + localized_folder).each do |strings_file|
      strings_path = STRINGS_PATH + &quot;/&quot; + localized_folder + &quot;/&quot; + strings_file
      if (NON_XIB_LOCALIZED_FILES.include?(strings_file))
        %x[cp \&quot;#{strings_path}\&quot; \&quot;#{localized_folder}.lproj/#{strings_file}\&quot;]
        puts &quot;COPIED #{strings_file} to #{localized_folder}&quot;
      elsif (!FILES_TO_IGNORE.include?(strings_file))
        filename = strings_file.slice(0,strings_file.length-8)
        source_xib = SOURCE_XIB_FOLDER + &quot;/&quot; + filename

        # Create a new localized XIB based on older localized version and two English versions.
        command = &quot;ibtool --previous-file \&quot;#{source_xib}.old.xib\&quot; &quot; +
                         &quot;--incremental-file \&quot;#{localized_folder}.lproj/#{filename}.old.xib\&quot; &quot; +
                         &quot;--strings-file \&quot;#{strings_path}\&quot; &quot; +
                         &quot;--localize-incremental &quot; +
                         &quot;--write \&quot;#{localized_folder}.lproj/#{filename}.xib\&quot; &quot; +
                         &quot;\&quot;#{source_xib}.new.xib\&quot;&quot;
        results = %x[#{command}]
        if results.length &gt; 0
          puts &quot;FAILURE: #{command}:\n#{results}&quot;
        else
          puts &quot;SUCCESS: #{localized_folder}.lproj/#{filename}.xib&quot;
        end

        # Generate .strings files for both localized versions to easily compare. Erase after.
        command = &quot;ibtool --generate-strings-file \&quot;#{localized_folder}.lproj/#{filename}.old.strings\&quot; \&quot;#{localized_folder}.lproj/#{filename}.old.xib\&quot;&quot;
        results = %x[#{command}]
        command = &quot;ibtool --generate-strings-file \&quot;#{localized_folder}.lproj/#{filename}.new.strings\&quot; \&quot;#{localized_folder}.lproj/#{filename}.xib\&quot;&quot;
        results = %x[#{command}]
      end
    end
  end
end
</pre>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/nHcao0KDC2Y" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/08/10/ibtool-scripts-on-google-code/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/08/10/ibtool-scripts-on-google-code/</feedburner:origLink></item>
		<item>
		<title>Custom UITableViewCell from a XIB in Interface Builder</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/cdDa0t4YTWM/</link>
		<comments>http://www.bdunagan.com/2009/06/28/custom-uitableviewcell-from-a-xib-in-interface-builder/#comments</comments>
		<pubDate>Sun, 28 Jun 2009 20:15:46 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=293</guid>
		<description><![CDATA[
Looking around the App Store, I see most apps customize their UITableViews in a unique way. Flixster embeds movie posters and ratings, in addition to their titles. Tweetie integrates tweets, icons, usernames, and the date. GasBuddy lists service type, amount spent, gallons, and dollars per gallon in each row. Constructing these customized UITableViewCells is possible [...]]]></description>
			<content:encoded><![CDATA[<p>
Looking around the App Store, I see most apps customize their <tt>UITableViews</tt> in a unique way. Flixster embeds movie posters and ratings, in addition to their titles. Tweetie integrates tweets, icons, usernames, and the date. GasBuddy lists service type, amount spent, gallons, and dollars per gallon in each row. Constructing these customized <tt>UITableViewCells</tt> is possible in code, but leveraging Interface Builder&#8217;s drag-and-drop interface is far more fun. Thanks to Bill Dudney for talking about one approach to this on <a href="http://bill.dudney.net/roller/objc/entry/uitableview_from_a_nib_file">his blog</a> and to StackOverflow for <a href="http://stackoverflow.com/questions/540345/lets-resolve-the-uitableviewcell-xib-question/541425">covering this topic</a>.
</p>
<p>
Creating a custom <tt>UITableViewCell</tt> using Interface Builder is straight-forward.
</p>
<ol>
<li>In Xcode, create a new <tt>UITableViewCell</tt> subclass and add the desired <tt>IBOutlets</tt> to the header file.
<li>In Interface Builder, create an &#8220;Empty&#8221; XIB from the Cocoa Touch palette.
<li>Drag a UITableViewCell from the Library into it, configure the class to be your new custom <tt>UITableViewCell</tt> subclass, and give it the appropriate identifier.
<li>Add the desired elements to the <tt>UITableViewCell</tt> and connect the subclass&#8217;s outlets to them.
<li>To instantiate the cells using the <tt>UIViewController</tt> approach, set class of the new XIB&#8217;s &#8220;File&#8217;s Owner&#8221; to be &#8220;UIViewController&#8221;, and connect its <tt>view</tt> outlet to the customized <tt>UITableViewCell</tt>.
</ol>
<p>
The XIB is set up, but there are two approaches to instantiating the new customized cell from that XIB. When I was at WWDC a couple weeks ago, I confirmed with one of the Interface Builder engineers at the IB Lab that both work just fine. (He did repeatedly ask if I was using <tt>UITableView:dequeueReusableCellWithIdentifier:</tt>, just to make sure.)
</p>
<h3><tt>UIViewController</tt></h3>
<p>
One approach is to create a temporary <tt>UIViewController</tt> each time you need a new cell. By setting up the XIB the way we did, the temporary <tt>UIViewController</tt> has the cell as its view attribute. After we grab a pointer to that, we can release the view controller.
</p>
<pre class="brush: cpp;">
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@&quot;BDCustomCell&quot;];
    if (cell == nil) {
		// Create a temporary UIViewController to instantiate the custom cell.
        UIViewController *temporaryController = [[UIViewController alloc] initWithNibName:@&quot;BDCustomCell&quot; bundle:nil];
		// Grab a pointer to the custom cell.
        cell = (BDCustomCell *)temporaryController.view;
		// Release the temporary UIViewController.
        [temporaryController release];
    }

    return cell;
}
</pre>
<h3><tt>NSBundle:loadNibNamed:owner:options:</tt></h3>
<p>
Another approach is to load the NIB file and grab the cell directly, as it&#8217;s the only top-level object in the NIB.
</p>
<pre class="brush: cpp;">
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@&quot;BDCustomCell&quot;];
    if (cell == nil) {
		// Load the top-level objects from the custom cell XIB.
        NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@&quot;BDCustomCell&quot; owner:self options:nil];
		// Grab a pointer to the first object (presumably the custom cell, as that's all the XIB should contain).
		cell = [topLevelObjects objectAtIndex:0];
    }

    return cell;
}
</pre>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/cdDa0t4YTWM" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/06/28/custom-uitableviewcell-from-a-xib-in-interface-builder/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/06/28/custom-uitableviewcell-from-a-xib-in-interface-builder/</feedburner:origLink></item>
		<item>
		<title>ibtool Caveats</title>
		<link>http://feedproxy.google.com/~r/FillTheVoid/~3/MbyxlHl14vM/</link>
		<comments>http://www.bdunagan.com/2009/06/27/ibtool-caveats/#comments</comments>
		<pubDate>Sun, 28 Jun 2009 01:35:15 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=264</guid>
		<description><![CDATA[
I&#8217;ve previously covered how Apple&#8217;s command-line utility ibtool can be used to automate localization tasks: generating a .strings file, localizing an English XIB, and incrementally updating a localized XIB. It&#8217;s a fantastic tool, included in the Xcode development installation, and works well for both Mac OS X applications and iPhone apps. I use this automation [...]]]></description>
			<content:encoded><![CDATA[<p>
I&#8217;ve previously covered how Apple&#8217;s command-line utility <tt>ibtool</tt> can be used to automate localization tasks: <a href="http://www.bdunagan.com/2009/03/15/ibtool-localization-made-easy/">generating a <tt>.strings</tt> file</a>, <a href="http://www.bdunagan.com/2009/03/15/ibtool-localization-made-easy/">localizing an English XIB</a>, and <a href="http://www.bdunagan.com/2009/04/15/ibtool-localization-made-easier/">incrementally updating a localized XIB</a>. It&#8217;s a fantastic tool, included in the Xcode development installation, and works well for both Mac OS X applications and iPhone apps. I use this automation extensively on Mac OS X (10.5): 14 XIBs across 11 languages means many, many files to keep in sync. However, the heavy use has exposed an annoying bug in <tt>ibtool</tt>: silent errors.
</p>
<h3>&#8220;Could not be parsed&#8221;</h3>
<p>
When <tt>ibtool</tt> encounters an expected bad character, like an unescaped double-quote, it verbally fails, supplying the message below.
</p>
<pre>
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
	&lt;key&gt;com.apple.ibtool.errors&lt;/key&gt;
	&lt;array&gt;
		&lt;dict&gt;
			&lt;key&gt;description&lt;/key&gt;
			&lt;string&gt;ibtool failed with exception: The stringsfile MainMenu.ES.strings could not be parsed.&lt;/string&gt;
		&lt;/dict&gt;
	&lt;/array&gt;
&lt;/dict&gt;
&lt;/plist&gt;
</pre>
<p/>
<p>
This error is helpful, but it doesn&#8217;t identify the offending line. To do that, we can use another Apple command-line tool: <a href="http://developer.apple.com/documentation/Darwin/Reference/ManPages/man1/plutil.1.html"><tt>plutil</tt></a>. (Thanks to <a href="http://cocoamusings.blogspot.com/2008/02/localizing-cocoa-software.html">Cocoa Musings</a> for pointing this out.) <tt>plutil</tt> can parse a <tt>.strings</tt> file and provide the line the bad character was on. It&#8217;s a very useful program when the <tt>.strings</tt> file that failed has 3K lines, and the problem is a single unescaped double-quote. <tt>ibtool</tt> generates these verbal parse errors on all alphanumeric characters and a number of others (<tt>" ' : . / - _ $</tt>).
</p>
<h3>Silent Errors</h3>
<p>
Unfortunately, <tt>ibtool</tt> (and <tt>plutil</tt>) fails silently on these other characters:
</p>
<pre>
{ } [ ] ; , < > ? \ | = + ! ` ~ @ # % ^ &#038; * ( )
</pre>
<p/>
<p>
Let me say that again: <tt>ibtool</tt> fails <i>silently</i>. The program stops incorporating localized strings when it encounters one of these characters, but it does not produce an error. The bug becomes particularly nasty when scaling up to 150 localized XIBs. I only discovered the bug when I found a single XIB incorrectly localized and tracked it back to a set of brackets randomly inserted into a <tt>.strings</tt> file.
</p>
<p>
So, I wrote a ruby script, <tt>validate_strings_files.rb</tt>, to validate <tt>.strings</tt> files more completely. The script parses through a directory of <tt>.strings</tt> files and produces verbal errors, with line numbers, for any out-of-place characters. It works well on the 11 languages I deal with. An error that would silently fail in <tt>ibtool</tt> would produce the following result with the script:
</p>
<pre>
VERIFYING FILE MainMenu.ES.strings
MainMenu.ES.strings (328): @
</pre>
<p/>
<p>
Below is the code for <tt>validate_strings_files.rb</tt>.
</p>
<pre class="brush: ruby;">
#!/usr/local/bin/ruby
# encoding: UTF-8

# NOTE: requires ruby 1.9
# MIT license

# validate_strings_files.rb
# This script reads through the .strings files in a directory and identifies any bad characters inside,
# both those that ibtool would catch and those that ibtool would miss.
# * ibtool verbal errors: &quot; ' : . / - _ $
# * ibtool silent errors: { } [ ] ; , &lt; &gt; ? \ | = + ! ` ~ @ # % ^ &amp; * ( )

require 'FileUtils'

# Check for arguments.
if ARGV.length != 1
  puts &quot;Usage: ruby validate_strings_files.rb path_to_strings&quot;
  exit
end

# Get path argument and 'cd' to that path.
PATH = ARGV[0]
FileUtils.cd(PATH)

def verify_file(file)
  line_number = 0
  is_multi_line_comment = false

  # Use general method unless file encoding is UTF-16.
  file_handle = File.new(file, &quot;r&quot;)
  # file_handle = File.new(file, &quot;r:UTF-16LE:UTF-8&quot;) (See http://blog.grayproductions.net/articles/ruby_19s_three_default_encodings)

  puts &quot;VERIFYING FILE #{file}&quot;
  while (!file_handle.eof?)
    has_seen_equals_sign = false
    has_seen_semi_colon = false
    is_single_line_comment = false
    is_string = false
    line_number += 1
    previous_char = nil
    second_previous_char = nil

    line = file_handle.readline
    # Use each_char (rather than each_byte) to support unicode.
    line.each_char do |char|
      # Notes:
      # * line can only be 2 strings, N multi-line comments, 1 equals sign (=), 1 semi-colon (;)
      # * ignore character if it's in a string
      # * single-line comments are ended by \n
      # * multi-line comments are ended by */

      if is_string || is_single_line_comment || is_multi_line_comment
        if is_string &amp;&amp; previous_char != &quot;\\&quot; &amp;&amp; char == &quot;\&quot;&quot;
          is_string = false
        elsif is_string &amp;&amp; second_previous_char == &quot;\\&quot; &amp;&amp; previous_char == &quot;\\&quot; &amp;&amp; char == &quot;\&quot;&quot;
          is_string = false
        elsif is_single_line_comment &amp;&amp; char == &quot;\n&quot;
          is_single_line_comment = false
        elsif is_multi_line_comment &amp;&amp; previous_char == &quot;*&quot; &amp;&amp; char == &quot;/&quot;
          is_multi_line_comment = false
        end
        # Ignore.
      elsif previous_char == nil &amp;&amp; char == &quot;/&quot;
        # Ignore.
      elsif previous_char == &quot;/&quot; &amp;&amp; char == &quot;/&quot;
        is_single_line_comment = true
      elsif previous_char == &quot;/&quot; &amp;&amp; char == &quot;*&quot;
        is_multi_line_comment = true
      elsif !has_seen_equals_sign &amp;&amp; char == &quot;=&quot;
        has_seen_equals_sign = true
      elsif !has_seen_semi_colon &amp;&amp; char == &quot;;&quot;
        has_seen_semi_colon = true
      elsif char == &quot; &quot; ||char == &quot;\n&quot;  || char == &quot;\r&quot;
        # Ignore.
      elsif char == &quot;\&quot;&quot;
        is_string = true
      elsif char == &quot;\000&quot;
        # Ignore unicode padding.
      elsif line_number == 1 &amp;&amp; previous_char == nil &amp;&amp; char == &quot;\377&quot;
        # Ignore unicode file header.
      elsif line_number == 1 &amp;&amp; previous_char == nil &amp;&amp; char == &quot;\376&quot;
        # Ignore unicode file header.
      else
        puts &quot;#{file} (#{line_number}): #{char}&quot;
      end

      if char != &quot;\000&quot; &amp;&amp; char != &quot;\377&quot; &amp;&amp; char != &quot;\376&quot;
        # Save previous character if it's not unicode padding.
        second_previous_char = previous_char
        previous_char = char
      end
    end
  end
end

# Iterate through the current directory.
Dir.entries(&quot;.&quot;).each do |file|
  filename = file.slice(0,file.length-8)
  extension = file.slice(file.length-8,file.length)
  # Only deal with .strings.
  if (extension == &quot;.strings&quot;)
    # Read the file and identify any bad characters.
    verify_file(file)
  end
end
</pre>
<p>
One important sidenote is Unicode support. The script works with Unicode characters but not universally. I have two lines to allow complete Unicode support. The first works on most languages, and the second is used for UTF-16 languages, like Japanese (JA).
</p>
<pre class="brush: ruby;">
# Use general method unless file encoding is UTF-16.
file_handle = File.new(file, &quot;r&quot;)
# file_handle = File.new(file, &quot;r:UTF-16LE:UTF-8&quot;)
</pre>
<p>
Keep in mind that <a href="http://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.1-p129.tar.gz">Ruby 1.9</a> is required because of its Unicode support; refer to the <a href="http://hivelogic.com/articles/view/ruby-rails-leopard#ruby">Hivelogic article</a> for compilation instructions. The <tt>r:UTF-16LE:UTF-8</tt> code is from <a href="http://blog.grayproductions.net/articles/ruby_19s_three_default_encodings">this article on Ruby encodings</a>.
</p>
<h3>&#8220;Class mismatch&#8221;</h3>
<p>
One other error I&#8217;ve encountered is &#8220;class mismatch&#8221;. <tt>ibtool</tt> produces this error when the class of a UI element associated with a specific <tt>ObjectID</tt> changes. For instance, imagine two languages were modified independently, and both had a UI element added to them. The first might be an <tt>NSTextField</tt> whereas the second might be an <tt>NSMenuItem</tt>; however, the IDs might be the same. The two entries in the .strings files would look like so:</p>
<p><tt>/* Class = "NSTextField"; title = "First Text"; ObjectID = "419"; */</tt><br />
<tt>/* Class = "NSMenuItem"; title = "Second Text"; ObjectID = "419"; */</tt></p>
<p>
Using the first as the base for localization, incoporating the strings into the second wouldn&#8217;t make any sense for this UI element. Luckily, <tt>ibtool</tt> produces the following error when trying to localize those XIBs:
</p>
<pre>
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
  &lt;key&gt;com.apple.ibtool.errors&lt;/key&gt;
  &lt;array&gt;
    &lt;dict&gt;
      &lt;key&gt;description&lt;/key&gt;
      &lt;string&gt;Class mismatch during incremental localization for Object ID: 419.  New Base has class: NSTextField.  Old Base has class: NSTextField.  Old Loc has class: NSMenuItem.&lt;/string&gt;
    &lt;/dict&gt;
  &lt;/array&gt;
&lt;/dict&gt;
&lt;/plist&gt;
</pre>
<p/>
<p>
<b>08/10/2009 Update:</b> I updated the validation script to account for escaped backslashes, such as <tt>"\\"," = "";</tt>.</p>
<img src="http://feeds.feedburner.com/~r/FillTheVoid/~4/MbyxlHl14vM" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2009/06/27/ibtool-caveats/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		<feedburner:origLink>http://www.bdunagan.com/2009/06/27/ibtool-caveats/</feedburner:origLink></item>
	</channel>
</rss>
