Advanced programming tips, tricks and hacks for Mac development in C/Objective-C and Cocoa.

Back to the Mac? 12 features from iOS I'd like to see in Lion

A few user-features of Mac OS X Lion have been announced but no Cocoa API changes have been publicly announced. However, I think there are dozens of non-user areas where the Cocoa Mac APIs could be improved by integrating approaches from Cocoa Touch APIs. What follows are a dozen areas where I'd like to see a more iOS approach in Mac OS X Lion.

Introduction

The changes I'm going to discuss fall four basic categories:

  • simplifying common tasks
  • making important classes simpler to customize
  • updating APIs on the Mac that seem to have been ignored since iOS appeared
  • making CoreAnimation good enough to be used for all views on the Mac

1. Converting NSColor to CGColor

This first point is a minor simplification of a common task but I think it illustrates the fact that iOS integrates CoreGraphics and other forms of drawing well, where this integration seems to be lacking on the Mac.

On iOS, converting a UIColor to a Core Graphics CGColor is a basic property accessor:

CGColor myCGColor = myUIColor.cgColor;

It's as simple as can be. On the Mac, it's as convoluted as I could have imagined:

// Ensure that the color is in the "generic" RGB space so we can safely get the components
NSColor *rgbColor = [self colorUsingColorSpaceName:NSCalibratedRGBColorSpace];

// Get the r, g, b, a components
CGFloat colorComponents[4];
[rgbColor getComponents:colorComponents];

// Create the CGColor
CGColorRef myCGColor =
    (CGColorRef)[(id)CGColorCreateGenericRGB(
        colorComponents[0],
        colorComponents[1],
        colorComponents[2],
        colorComponents[3])
    autorelease];

While I'm on the topic of colors, I don't think the documentation for NSColor is clear enough about the choice between "Device" colors or "Calibrated" colors in an application user-interface. On iOS, there are only device colors — so there's no question — but on the Mac, you can create a white NSColor as either:

NSColor *calibratedColor = [NSColor colorWithCalibratedWhite:1.0 alpha:1.0];

or

NSColor *deviceColor = [NSColor colorWithDeviceWhite:1.0 alpha:1.0];

The User Interface Guidelines, NSColorSpace and the NSColor documentation offer no information about which should be the default choice in an arbitrary application. It'd be nice to have a clearer statement about when each is appropriate.

Edit: as pointed out in the comments, Apple do have a line in the Color Programming Topics documentation that suggests that you use calibrated color spaces where possible.

2. A constructor for NSTextField that constructs it as a static label

Another simplification of a common task that I'd like to see is the ability to create a static text label in a single statement. On iOS, we have the UILabel subclass of NSView for non-editable text display.

UILabel *label = [[UILabel alloc] initWithFrame:labelFrame];

On the Mac, the editable NSTextField can be configured to handle non-editable display. Unfortunately, while there is an efficient way to construct NSTextField as a static text label in Interface Builder, there is no similarly efficient way in code:

NSTextView *label = [[NSTextField alloc] initWithFrame:labelFrame];
[label setEditable:NO];
[label setBezeled:NO];

Not a huge gripe but if you're constructing a view in code, the two extra lines for every label can become tiresome. An -initStaticLabelWithFrame: or +staticLabelWithFrame: method would be nice.

3. Simplified methods for common actions, e.g. -[UIImage drawInRect:]

A final type of common task I'd like to see simplified is offering simpler methods for common cases.

One of the most prominent examples of simpler methods that I'd like to see is for drawing an image. To draw a UIImage in iOS we use a simple method:

[myImage drawInRect:someRect];

On the Mac:

[myImage
    drawInRect:someRect
    fromRect:NSZeroRect
    operation:NSCompositeSourceOver
    fraction:1.0
    respectFlipped:YES
    hints:nil];

I realize that in both cases it's only one method. I also realize that the Mac example is significantly more capable. However, the functionality of the iOS method is overwhelming the common case and I think that this option should be added to the Mac as well, to recognize that this is the common path.

4. Resizable NSImage

Another NSImage/UIImage point — but now in the topic area of making important classes easier to customize — it would be really good to see the method:

- (UIImage *)stretchableImageWithLeftCapWidth:(NSInteger)leftCapWidth
    topCapHeight:(NSInteger)topCapHeight

brought from iOS to the Mac. If you've never used this method before, it allows you to take a square image — like a button image — and label and specify the boundary of the image (the left and top "caps") so that when the image is stretched to fit a new size, the cap areas are preserved and only the middle of the image is stretched.

On the Mac, you can use the function NSDrawNinePartImage() but while this function is significantly more configurable, it lacks the ability to be used anywhere you use an NSImage — so you can't use it as the background to an NSButton, you must subclass NSButtonCell and handle the drawing manually.

5. Setting the text color on an NSButton

The second "making important classes easier to customize" point is setting text color on a button. For UIButton it looks like this:

button.titleLabel.textColor = [UIColor whiteColor];

On the Mac there are no standard methods and you have to change the attributed string for the NSButton's title:

NSMutableAttributedString *title =
    [[[NSMutableAttributedString alloc] 
        initWithAttributedString:[button attributedTitle]]
    autorelease];
[title
    addAttribute:NSForegroundColorAttributeName 
    value:[NSColor whiteColor]
    range:NSMakeRange(0, [title length])];
[title fixAttributesInRange:range];
[button setAttributedTitle:title];

Not only is this ugly, but you may have to redo it if the title changes.

6. Support for white on black in other controls

It should be much easier is changing the color of other standard controls too. Specifically, I think it should be possible to place any control on a black background and make it visible.

Do you want a white "spinner" (indeterminate progress indicator) to go on a black background? On iOS, all you need is:

UIActivityIndicatorView *indicator =
    [[[UIActivityIndicatorView alloc]
        initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]
    autorelease];

If you want to make the NSProgressIndicator on the make white... you can't. You need to draw the entire control yourself.

7. An NSTableView equivalent that uses NSView for rows

The final "making important classes easier to customize" point is about NSTableView: we need an NSTableView like class on the Mac that handles the same column and row operations but contains proper NSViews instead of NSCell.

I've spoken about this desire before. The idea is pretty straightforward: UITableView creates a proper UITableViewCell (a subclass of UIView) for every row — this means that rows in a UITableView can behave like any UIView — they can be loaded from a NIB by a UIViewController, they can receive touches, contain buttons and be animated.

The iOS UITableView lacks bindings and doesn't have columns at all but in nearly every other respect, it is superior to the options on the Mac.

The NSCell that draws an NSTableView on the Mac is just limited. It can't animate, it can't contain regular views or buttons or receive clicks independent of the NSTableView. While using just one lightweight cell object per column may have made sense in the 1990's, it seems unnecessarily limiting in 2010.

There is certainly NSCollectionView which handles grids of actual NSViews but this class would need some work in terms of efficiency if it is to replace an NSTableView.

What's wrong with NSCollectionView? Simple: it constructs every view, including those that aren't visible. If your NSCollectionView contains 1 million views, this isn't going to work well. Further: NSCollectionView doesn't handle table-like interaction (you can't use column or row operations by default).

The reality is that NSCollectionView is designed to fill a slightly different niche; what we really need is a better NSTableView.

More generally than NSTableView: the entire NSCell concept seems dated. I would not be sad to see Apple deprecate them entirely. In some respects: leaving them out of UIKit is a mild form of deprecation. Now they just need to take the next step and start offering a way to use AppKit without NSCells.

8. Get rid of NSNib

Now to the first of the APIs that I think were updated when they were implemented in iOS but these updates never seemed to make it into Mac OS itself.

The first is a fairly mild point: what is the purpose of the NSNib class on the Mac? Technically, the answer is "it loads and instantiates NIB files". It also provides the ability to cache the loaded NIB for faster instantiation. In reality though, iOS does all of this work without needing a new class.

Specifically in this case, I'm looking at loading a NIB file and getting the array of top level objects.

Both iOS and Mac OS X can load a NIB through NSBundle without getting the top level array but on Mac OS X (connecting only to the "owner" object) but getting full access to the created objects requires using NSNib. In iOS though, you can do this directly from NSBundle, making the existence of the NSNib class seem a little pointless.

On iOS:

contentArray =
    [[[NSBundle mainBundle]
        loadNibNamed:nibName
        owner:self
        options:nil]
    retain];

On the Mac:

NSNib *nib =
    [[[NSNib alloc]
        initWithNibNamed:nibName
        bundle:[NSBundle mainBundle]]
    autorelease];
[nib instantiateNibWithOwner:self
    topLevelObjects:&contentArray];
[contentArray retain];

You could probably argue that the Mac implementation lets you cache the NSNib and reinstantiate again later. The reality is: the iOS version seems to handle this cacheing behind the scenes, automatically. Less code, more powerful; it's better. We can get rid of NSNib on the Mac.

Edit: it appears I overlooked the fact that iOS introduced UINib in 4.0 — so Apple are bringing the Mac approach to iOS instead of the other way around. So I guess this is a point where I simply disagree; I think that the cacheing should be done automatically in NSBundle (so that the simplest approach is also the best). This could be combined with optional cache/don't cache flags passed into the "options" parameter if you need finer control.

9. Language Rotor for Text-To-Speech

This is probably the most "user" feature of the points in this post but iOS has had dramatic improvements in text-to-speech relative to the Mac.

On iOS devices that support VoiceOver, the voice in Australia (where I live and work) has an Australian accent. This is due to the "Language Rotor" settings on iOS.

Now, I realize that I'm not blind and the only time I ever need to turn on VoiceOver is when I'm testing the UIAccessibility/NSAccessibility protocol implementations in my applications but whenever I do this on the Mac the voice doesn't sound right.

10. Update OpenGL!

Do I need to explain this one?

In iOS, you have access to OpenGL ES 2.0 — the current version of the OpenGL ES standard.

I realize that OpenGL on the Mac is not the OpenGL ES version. I also realize that it's version number is OpenGL 2.1 which is 0.1 higher than the iOS version. Obviously, I'm not suggesting that the Mac adopt exactly the same version of OpenGL that iOS uses.

What I mean is that iOS is using the up-to-date, relevant version of OpenGL whereas the OpenGL 2.1 standard on the Mac was superceeded 2 and a half years ago but Apple have not updated to support it. It is a complete embarrassment to the Mac as a platform. The current version of OpenGL for computers, OpenGL 4.1, was released in July this year. There are already cards available for the Mac that support OpenGL 4.1 on other operating systems but can't due so on the Mac because the OS does not support it.

11. Integration between CoreAnimation and standard views

The last two features from iOS I'd like to see both relate to making CALayer-backed views useable by default on the Mac.

Major views should offer animated changes for common actions. If you want to animate a new row into a UITableView on iOS, you only need one line:

[myTableView
    insertRowsAtIndexPaths:someIndexPath
    withRowAnimation:UITableViewRowAnimationLeft];

On the Mac — there certainly isn't a single line option. In fact, there's no easy solution at all, since rows are drawn by NSCell and don't generally have CALayers of their own (even when CoreAnimation is enabled for the NSTableView).

12. Fix the horrible text rendering when CoreAnimation is enabled

The following image shows Helvetica Bold 18pt, rendered on the Mac in two different ways.

Both are an NSTextField over a custom drawn gradient but where the top row uses a regular, non-CALayer-backed view, the bottom row uses CALayer-backed views for the entire window.

CoreAnimationText.png

At first glance, the second row may simply look less fat but if you look closely, you'll see that it is more irregular, less proportionate, badly kerned and far less smooth.

This horrible text rendering affects all text in CALayer-backed views.

On iOS, which always uses CoreAnimation, the text rendering looks very similar to the top example (except that iOS doesn't use sub-pixel antialiasing). Clearly, there is a way to improve the situation with CALayers.

This text quality problem with CoreAnimation has been a burden since CoreAnimation first appeared in Mac OS X 10.5 and it remains the biggest disincentive towards using it on the Mac. It really needs to be addressed.

Conclusion

These points illustrate my feeling that the Mac Cocoa APIs, AppKit in particular, need some general tidying up (and in some cases, a major refresh) if they are to feel anything other than dated with respect to their iOS equivalents.

While some of these features would be major changes, many simply reflect the fact that AppKit could do with a well-applied coat of paint in many areas. These minor changes can already be addressed by each programmer (a category or a little extra code) but if these sorts of additions are considered commonplace, I think it's an indication that they should be rolled into the default API.

Some of these features might require deprecating major classes in favor of more modern implementations — possibly NSTableView and all NSCells. I wouldn't be sad to see them replaced by something that feels more up-to-date.

A Cocoa application for running scripts

Last week, I showed a bash script that you can use to build, tag and package a Mac application. As I'll show you this time though, I prefer to use a compiled Mac application to do the same thing. The important code here is a set of classes that support script-like invocations of other programs and support for a structured build-log based on the results of each step.

Introduction

There are lots of different reasons to run scripts — configuring some open source project, packaging applications for deployment, arranging windows in your Xcode workspace, processing metadata from your iTunes collection, generating signatures for files, processing crash logs, etc.

But every time I'm forced to use a command-line script, I feel a little sad inside. I'm an application developer (at least some of the time) and a small, cramped, single-font, single-spaced, text-filled box seems like the wrong way to interact with anything that isn't a text editor. If I'm writing unformatted text, then an unformatted text window is the right interaction metaphor. For every other action, it reminds me that programmers often solve a whole world of construction problems using a toolbox filled entirely with hammers.

scriptrunning.png

Another random wall of text. Everything must be fine. Or broken. What's going on?

When running a script — particularly one that can cause large numbers of errors or one that I might need to re-run repeatedly — the progress information, results presentation and error handling of a command-line script is truly sub-par.

IDE build logs do a much better job in this regard. As an example, here's Xcode building the same code as the previous Terminal screenshot:

xcodebuildlog.png

Oh! I was building ffmpeg. Badly.

With a build log approach to display, you can see all the steps involved. The steps themselves are shown in a compact form.

Running a script in a compiled application

This is the key rationale behind this week's variation on the build script: a structure log with errors and warnings.

buildwitherror.png

The Cocoa Build Script application showing the display with progress, errors and error highlighting.

You can download the complete sample project used in this post here CocoaScript.zip (255kb)

This program runs a generic series of steps and captures their standard output and standard error (or for non-process steps, whatever they choose to emit as their output and error).

In the sample application, the steps executed by the script are largely the same as last week's bash script — it's a build, version, commit, package script for deploying a basic Mac application. Yes, the application is entirely capable of packaging itself.

However, running a deployment script is not the only possible use for the classes in this project. The step classes are generic enough that most common script-like operations can be handled or you can choose to run standard Cocoa code as a step. I use variations of these classes as a lightweight job running framework for a number of different (mostly in-house) purposes.

To be clear: when I use the word "script" in this post, I'm referring to a series of steps that are largely invocations of other utility programs (the role that "shell scripts" normally fullfil). I am not using the word script to mean an interpreted programming language. The steps are not extracted by interpreting a text file at runtime — the actual program is compiled Cocoa/Objective-C.

Please don't confuse this with efforts like F-Script that actually execute an interpreted script within the Cocoa runtime.

Explaining the design of the application

The script itself is contained in the Script.m file (top of the Group Tree in the Xcode project).

Document and top level control

Since this application is a single window application, most of the application is controlled from the CocoaScriptWindowController. On construction, this window controller constructs the ScriptQueue (which is a subclass of NSOperationQueue). This ScriptQueue will run the step pipeline during the application.

When the queue is started (as happens automatically on startup or when the user restarts the application), the CocoaScriptWindowController calls the ScriptSteps() function to get the array of steps from the Script.m file and inserts the steps into the queue.

This steps array is the document of the application.

Observers

While the queue is running, the controllers observe progress in the following ways:

  • the CocoaScriptWindowController observes the number of items left in the queue
  • the NSArrayController (connected to the steps array in Interface Builder) observes the original steps in the queue
  • the array of ScriptStepCollectionViewItems (which are constructed automatically by the NSCollectionView due to its binding with the NSArrayController) observe the selected status of each step and the completion/error status of each step
Views

The ScriptStepView draws each row in the NSCollectionView on the left. Each ScriptStepView is bound to some data through the ScriptStepCollectionViewItem and other data (like step completion status) is pushed at it by the ScriptStepCollectionViewItem.

A standard NSTextView draws the text on the right. Its content is switched by the CocoaScriptWindowController based on the current selection and whether the Step Output or the Step Error is selected. The steps themselves generate their output as an NSTextStorage object which can be easily and efficiently connected to the NSTextView in this manner.

ScriptQueue and ScriptSteps

The queue and steps handle the functional pipeline of the application.

The queue is an NSOperationQueue and runs all steps in the global concurrent thread pool. However, unlike a standard NSOperationQueue, it makes all tasks dependent on the immediately preceding step by default (so steps are not concurrent by default). Steps can override this behavior by setting their concurrentStep (a step with which they are allowed to run in parallel). If steps set themselves as their concurrentStep, then they will have no dependencies.

The queue also has a list of cleanupSteps. These are a series of steps that are run if the queue is cancelled or terminates with an error (in the current version, they are not run if the script completes normally). You can push cancel steps on to the start or end of this list.

Of the ScriptSteps themselves, the most common is probably the TaskStep. This runs a process with an array of arguments and captures the standard output and standard error. You can pipe the output of one TaskStep into another (destinations of a pipe must be inserted into the array of steps after their source for the automatic concurrency between source and destination to work). You can also set regular expressions for the standard output or standard error which, if matched, will mark the step as having warnings or errors.

Most of the other steps are based on either BlockStep or ConditionalStep. BlockStep runs a C block for the step (so you can execute arbitrary Objective-C code). ConditionalStep does the same but returns a YES/NO condition. If a ConditionalStep returns NO, it will cancel any step that is "predicated" on the condition.

Threading notes

The steps run in the global concurrent queue (i.e. various threads). For the most part, they are not thread-safe while they are running and do not expect to be accessed after they are added to the queue. If you need to communicate between steps, you'll need to handle concurrency issues (have a look at the taskStartedCondition used in TaskStep to allow one TaskStep to pipe data to another for an example).

The textual output and error output from each step is an NSTextStorage object. These may only be accessed from the main thread (NSTextStorage requirement). If you look at ScriptStep.m, you'll notice that most of the code in this file is actually focussed on letting the ScriptStep (which runs from the global concurrent thread pool) post text changes (append, replace, change attributes) to the main thread.

State values and ScriptValues

Inside a step, you can access state values. This key value storage on the ScriptQueue acts as the variable storage during execution. If a step needs to pass data to another step, it can do so by storing data in a state value.

Looking through the Script.m file, it is important to remember that all code inside each step happens at script execution time — which happens some time after the ScriptSteps() function returns. However, you can only access the state values while the script is executing. When you're still assembling the array of steps in the ScriptSteps() function, the ScriptQueue state storage is not accessible.

To allow you to pass runtime state values into a step, many steps will accept ScriptValue objects instead of NSStrings for some parameters. A ScriptValue object is a placeholder for a state value. At execution time it is resolved to the actual state value.

Conclusion

You can download the complete sample project used in this post here CocoaScript.zip (255kb)

The steps used in this sample application's script are largely the same as last week's bash script. You can read the two side-by-side to see how different operations are done in each.

The Script.m file is about 3 times longer than the bash script was last week (550 versus 160 lines). While Cocoa is slightly wordier than bash, about 30% of the line difference is actually just whitespace due to my code formatting style. In this version, I also configure each step to have careful error handling and user feedback which extends the length of the script somewhat. There's also more commenting in this version.

The entire project is a little under 5000 lines (including comments and whitespace), which I think makes it one of the largest projects I've presented in a post. It's a little hard to cover all the aspects but there's plenty of code snippets inside to look at (it's all at least partially commented). If you're looking for an example on NSCollectionView and NSCollectionViewItem, NSOperationQueue and NSOperation, NSTextStorage and NSTextView, NSTask or NSPredicate, then there's code to be found within.

I'm not entirely sure whether other people are likely to enjoy the idea of running script-like tasks in a compiled Cocoa application as much as I do — I realize I'm probably pretty strange in this regard. However, I think there's merit in considering the presentation of results as part of the expectations of a script; especially where you want clear feedback about how a process is progressing or why a step failed. That's the real strength of this approach: highly configurable error checking and structured feedback for all steps.

A deployment script for a generic Cocoa Mac application

Deployment for a Cocoa Mac application normally involves a few common steps: committing code into a repository, updating version numbers and packaging the application as a DMG disk image. In this post, I'll show you a combination bash/perl/Applescript to handle all these tasks in a single script.

Deployment steps

Standard Xcode build templates will build your code but don't really offer any help beyond that point. Deploying your application normally requires a few extra steps beyond what the standard templates provide.

While you can simply make a ZIP archive out of the application in your Release directory and call it done, there's normally a few more steps that you should take when deploying an application.

1. Update your version number

Obviously, this isn't a hard step but it is easy to forget since the user-facing version number (build numbers not included) must be deliberately updated when you decide a build is ready.

2. Commit your code into a version control system

This step should be obvious: it protects your time investment by ensuring that your code doesn't disappear.

And yet I still visit small or single-programmer offices where all the code is just kept in a directory and the whole directory is duplicated from version to version or to play with features. Seriously, that is not how you should manage things. You may have your disk backed up, you may keep copies of your builds but these things are only complimentary to maintaining your repository.

How did we implement this feature in version 1.2 (and where's the old code)? Why was this code changed and what did it do before? Do we have a copy of version 3.5 of the application anywhere?

These are the questions that are best answered by not only having a code repository but having it properly tagged for every build. I won't get into philosophies about how frequently you should commit between releases or how you should tag bug fixes and feature changes but the absolute minimum for any system should be that you tag your releases. Your deployment script should make this mandatory.

Incidentally, newer distributed repository version control programs like git make creating a repository for your code so much simpler. Instead of needing to set up a centralized location, you can casually make any folder its own local repository (i.e. just run "git init" in the directory) and worry about whether and where to locate an official or shared repository later.

Frustrated at Xcode's horrible version control system support? Can't remember all the git commands? Don't like git's command-line interface? I certainly don't. Fortunately, Mac apps like GitX are simple, pretty and work well.

3. Make certain you have a clean build

From time-to-time, you will encounter problems with builds that are only fixed by cleaning and rebuilding. One of the biggest examples of this are assets that you deliberately remove from the build — these never get removed from the build directory unless you clean first, then build. Without a step to clean the directory first, your build may not be exactly what you think it should be.

4. Package the application in a DMG file

Mostly for reasons of aesthetic presentation, Mac applications that don't require an installer are normally deployed as DMG disk images. These can be a little fiddly to create, adjust aesthetically and then create a compressed version for distribution.

Fortunately, with a little Applescripting, we can automate this process too.

A big ole Bash script

Here then is a bash script to handle all of the above steps. It's an annoying diversion into another language for a C/Obj-C/C++ programmer but some things (especially setting folder view options) need to be done a specific way.

A script of this sort is the traditional way that this type of deployment is handled. However, it is actually not how I handle my deployments (but I'm a little weird in this respect). Next week, I'll show you the code I use for deployment.

Assumptions in this script

There's a few assumptions here. While they are normally valid assumptions if you create your project using default Cocoa Mac Application template, there are certainly cases where they won't apply and you'll need to tweak the script a little.

  1. This script requires 1 parameter: the .xcodeproj file you want to build.
  2. The target you want to build must have the same name as the project (minus the .xcodeproj extension).
  3. The Info.plist for the application must have the same name as the project (minus the .xcodeproj extension) with the suffix "-Info.plist".
  4. The application build has the same name as the project (minus the .xcodeproj extension).
  5. The deployment build is the "Release" build and the build project directory is the build/Release directory.
  6. You use git for your repository (although this script will continue if git is not installed).
  7. The background image for your DMG folder is a 400x300px PNG named background.png in the same folder as the .xcodeproj file (although this script will skip background image steps if the background.png is missing).
  8. The deployment DMG file will be saved to the Desktop with the same name as the project (minus the .xcodeproj extension) with the suffix ".dmg" (build will fail if there's already something at this location).
The script
#!/bin/bash

if [ ! "${1}" ]; then
    echo "usage: $0 xcode_project_path"
    exit
fi

XCODE_PROJECT_PATH=$1
XCODE_DIRECTORY="`dirname "$1"`"
XCODE_PROJECT_NAME="`basename -s .xcodeproj "${XCODE_PROJECT_PATH}"`"

# xcodebuild needs to run from the project's directory so we'll move there
# and stay for the duration of this script
cd "${XCODE_DIRECTORY}"

# Check if git is installed
if [ `which git` ]; then
    echo "git is installed on this computer"

    # If git is installed, then require that the code be committed
    if [ "`git status -s 2>&1 | egrep '^\?\?|^ M|^A |^ D|^fatal:'`" ] ; then
        echo "Code is not committed into git. Commit into git before deployment."
        exit
    fi
    echo "Repository up-to-date."
fi

# !! Update: Changed from using the Perl Cocoa bridge to using PlistBuddy !!
# Use the perl to Objective-C bridge to get the version from the Info.plist
# You could easily use the python or ruby bridges to do the same thing
#CURRENT_VERSION="`echo 'use Foundation;
#$file = "'"${XCODE_PROJECT_NAME}"'-Info.plist";
#$plist = NSDictionary->dictionaryWithContentsOfFile_($file);
#$value = $plist->objectForKey_("CFBundleVersion");
#print $value->description()->UTF8String() . "\n";' | perl`"

# Use PlistBuddy instead of the perl to Cocoa bridge
CURRENT_VERSION="`/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' \
    "${XCODE_PROJECT_NAME}-Info.plist"`"

# Report the current version
echo "Current version is ${CURRENT_VERSION}"

# Prompt for a new version
read -p "Please enter the new version:
" NEW_VERSION

# !! Update: Changed from using the Perl Cocoa bridge to using PlistBuddy !!
# Use the bridge again to write the updated version back to the Info.plist
#echo 'use Foundation;
#$version = "'$NEW_VERSION'";
#$file = "'"${XCODE_PROJECT_NAME}"'-Info.plist";
#$plist = NSDictionary->dictionaryWithContentsOfFile_($file);
#$plist->setObject_forKey_($version, "CFBundleVersion");
#$plist->writeToFile_atomically_($file, "YES");' | perl

# Use PlistBuddy instead of the perl to Cocoa bridge
/usr/libexec/PlistBuddy -c "Set CFBundleVersion ${NEW_VERSION}" \
    "${XCODE_PROJECT_NAME}-Info.plist"

# Commit the updated Info.plist
if [ `which git` ]; then
    git commit -m "Updated Info.plist to version ${NEW_VERSION}" \
        "${XCODE_PROJECT_NAME}-Info.plist"
fi

# Clean the Release build
xcodebuild -configuration Release -target "${XCODE_PROJECT_NAME}" clean

# Build the Release build
if [ "`xcodebuild -configuration Release -target "${XCODE_PROJECT_NAME}" build \
     | egrep ' error:'`" ] ; then
    echo "Build failed."
    exit
fi

# Tag the repository now that we have a successful build
git tag "version-${NEW_VERSION}"

#########
# From this point onwards, the script is all about DMG packaging
#########

# Create a temporary directory to work in
TEMP_DIR="`mktemp -d "${TMPDIR}${XCODE_PROJECT_NAME}.XXXXX"`"

# Create the folder from which we'll make the disk image
DISK_IMAGE_SOURCE_PATH="${TEMP_DIR}/${XCODE_PROJECT_NAME}"
mkdir "${DISK_IMAGE_SOURCE_PATH}"

# Copy the application into the folder
cp -R "build/Release/${XCODE_PROJECT_NAME}.app" \
    "${DISK_IMAGE_SOURCE_PATH}/${XCODE_PROJECT_NAME}.app"

# Make a symlink to the Applications folder
# (so we can prompt the user to install the application)
ln -s "/Applications" "${DISK_IMAGE_SOURCE_PATH}/Applications"

# If a "background.png" file is present in the Xcode project directory,
# we'll use that for the background of the folder.
# An assumption is made in this script that the background image is 400x300px
# If you are using a different sized image, you'll need to adjust the
# placement and sizing parameters in the Applescript below
if [ -e "background.png" ]; then
    cp "background.png" \
        "${DISK_IMAGE_SOURCE_PATH}/background.png"
fi


# Create the read-write version of the disk image from the folder
# Also note the path at which the disk is mounted so we can open the disk
# to adjust its attributes
DISK_IMAGE_READWRITE_PATH="${DISK_IMAGE_SOURCE_PATH}-rw.dmg"
VOLUME_MOUNT_PATH="`hdiutil create -srcfolder "${DISK_IMAGE_SOURCE_PATH}" \
    -format UDRW -attach "${DISK_IMAGE_READWRITE_PATH}" | \
    sed -n 's/.*\(\/Volumes\/.*\)/\1/p'`"


# Now we use Applescript to tell the Finder to open the disk image,
# set the view options to a bare, icon arranged view
# set the background image (if present)
# and set the icon placements
if [ -e "background.png" ]; then
    echo '
    tell application "Finder"
        open ("'"${VOLUME_MOUNT_PATH}"'" as POSIX file)
        set statusbar visible of front window to false
        set toolbar visible of front window to false
        set view_options to the icon view options of front window
        set icon size of view_options to 96
        set arrangement of view_options to not arranged
        set the bounds of front window to {100, 100, 500, 400}
        set app_icon to item "'"${XCODE_PROJECT_NAME}"'" of front window
        set app_folder to item "Applications" of front window
        set background_image to item "background.png" of front window
        set background picture of view_options to item "background.png" of front window
        set position of background_image to {200, 200}
        set position of app_icon to {120, 100}
        set position of app_folder to {280, 100}
        set current view of front window to icon view
    end tell' | osascript
else
    echo '
    tell application "Finder"
        open ("'"${VOLUME_MOUNT_PATH}"'" as POSIX file)
        set statusbar visible of front window to false
        set toolbar visible of front window to false
        set view_options to the icon view options of front window
        set icon size of view_options to 96
        set arrangement of view_options to not arranged
        set the bounds of front window to {100, 100, 500, 400}
        set app_icon to item "'"${XCODE_PROJECT_NAME}"'" of front window
        set app_folder to item "Applications" of front window
        set position of app_icon to {120, 100}
        set position of app_folder to {280, 100}
        set current view of front window to icon view
    end tell' | osascript
fi

# Make the background.png file invisible
SetFile -a V "${VOLUME_MOUNT_PATH}/background.png"

# Eject the disk image so that we can convert it to a compressed format
hdiutil eject "${VOLUME_MOUNT_PATH}"

# Create the final, compressed disk image
hdiutil convert "${DISK_IMAGE_READWRITE_PATH}" -format UDBZ \
    -o "${HOME}/Desktop/${XCODE_PROJECT_NAME}.dmg"

# Remove the temp directory
rm -Rf "${TEMP_DIR}"

Conclusion

Ergh: a code-heavy post with neither C nor Objective-C.

Next week, I'll show you how to perform the same steps in a logging, reporting, error-handling Cocoa application.