Mac App Store guide

It’s no secret that the Mac App Store is a terrific new distribution channel for developers. Apple also provides plenty of documentation on how to prepare your app for submission.

Unfortunately, there’s not much information on how to create a product that can also be distributed through more traditional channels, such as your own product website. This guide will help you update your Xcode projects to make it as simple as possible to create products for both channels simultaneously.

Introduction

The basic strategy is to create two build targets: one that creates everything for your own website, and another that creates the stuff Apple requires for the Mac App Store. The examples were all written using Xcode 3, but can be adapted to newer versions as needed.

The build target for your own website will include the Sparkle framework for doing updates. The one for the Mac App Store will create signed code that does a cryptographic check of the license receipt.

Note that these techniques can be used even if you’re only doing distribution on the Mac App Store: beta testers will benefit from builds that don’t require a receipt before launching.

To take you through the entire process, I’m going to use a real world example: I originally wrote these instructions while preparing our Flare product for release. When you see “Flare”, think “MyApp”. Likewise, “Iconfactory” will be “MyCompany.”

Certificate Setup

Before setting things up, make sure that you have two certificates in your keychain:

3rd Party Mac Developer Installer: The Iconfactory
3rd Party Mac Developer Application: The Iconfactory

If you don’t already have these installed, you can get them from the Developer Certificate Utility in the Member Center on the Apple Developer website.

Project Settings

In Project Build Settings, under Packaging, enable Info.plist preprocessing with:

Info.plist Other Preprocessor Flags: -C
Preprocess Info.plist File: Checked

Do this for both the Debug and Release configurations.

Why do you want to preprocess your Info.plist file? Because there are subtle differences between the settings required for the Mac App Store and your own website. You could, of course, solve the problem by having two Info.plist files, but then you run the risk of them getting out of sync: what happens when you change the version number in one file and forget to do it in the other? Keeping things like document types consistent is tedious work: it’s much easier to configure your app with a single file.

Since things like checking license files and expiration dates are a pain when you’re debugging code, we’ll also define a DEBUG flag in the Debug configuration:

Other C Flags: -DDEBUG

Target Settings

You’ll want to create two build targets:

Flare
Flare App

It’s likely that you already have one target and can just duplicate it. The target “Flare” will be for your website, while “Flare App” is destined for the Mac App Store.

Make sure that both targets are using the same Info.plist source file for both the Debug and Release configurations:

Info.plist File: Flare-Info.plist

When you duplicate a target, Xcode “helpfully” creates a new Info.plist file for you. Since you’re going to be using a single file that’s customized using the preprocessor, you don’t need this second file and can delete it from the project folder.

It’s also important to remember that any new files that get added to the project now must be added to both targets. The goal is to keep both of them in sync.

Flare

In the target’s Link Binary With Libraries build phase, make sure that Sparkle.framework is included. If you’re following these steps just so you can have a beta build, leave this framework out.

Flare App

In Flare App’s Link Binary With Libraries build phase, make sure that Sparkle.framework is not included. You’ll want to make sure that IOKit.framework and Security.framework are included in the list (since they are used to determine the MAC address and check certificates during license verification.)

While you’re at it, make sure that Sparkle.framework is not in the target’s Copy Files build phase. Even if your app is not linking to Sparkle, a rejection awaits if the App Review team finds the framework in the application bundle.

Since we’re going to be adding code that checks which version is being built, add a new preprocessor flag to both the Debug and Release configurations for the target:

Other C Flags: -DMAC_APP_STORE

In your Objectve-C source code, you can now do things like this:

#ifdef MAC_APP_STORE
	BOOL validLicense = AppStoreLicenseCheck();
#else
	BOOL validLicense = MySuperSekretLicenseCheck();
#endif

App Store submissions must use signed code, so you’ll need set the following build setting in the Code Signing section:

Code Signing Identity: 3rd Party Mac Developer Application: The Iconfactory

You may also want to do this for the Flare target as well.

In the Linking section, you need to add the OpenSSL cryptographic library so the App Store license can be checked:

Other Linker Flags: -lcrypto

Do this for both the Release and Debug configurations since you’ll want to use Apple’s test_receipt while debugging your license verification code.

Finally, in the Target Build Settings, add the following under Packaging:

Info.plist Preprocessor Definitions: MAC_APP_STORE

Info.plist

Now you can update your Info.plist to work with preprocessor definitions. For example, the Flare-Info.plist is:

#define BUILD_VERSION 1.0
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>English</string>
	<key>CFBundleExecutable</key>
	<string>${EXECUTABLE_NAME}</string>
	<key>CFBundleIconFile</key>
	<string>app.icns</string>
	<key>CFBundleIdentifier</key>
#ifndef MAC_APP_STORE
	<string>com.artissoftware.mac.flare</string>
#else
	<string>com.artissoftware.flare</string>
#endif
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>${PRODUCT_NAME}</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>BUILD_VERSION</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleVersion</key>
	<string>BUILD_VERSION</string>
	<key>LSApplicationCategoryType</key>
	<string>public.app-category.photography</string>
	<key>LSMinimumSystemVersion</key>
#ifndef MAC_APP_STORE
	<string>${MACOSX_DEPLOYMENT_TARGET}</string>
#else
	<string>10.6.6</string>
#endif
	<key>NSHumanReadableCopyright</key>
	<string>Copyright © 2006-2011 The Iconfactory & Artis Software</string>
	<key>NSMainNibFile</key>
	<string>MainMenu</string>
	<key>NSPrincipalClass</key>
	<string>NSApplication</string>
#ifndef MAC_APP_STORE
	<key>SUFeedURL</key>
	<string>http://iconfactory.com/appcasts/Flare/appcast.xml</string>
	<key>SUExpectsDSASignature</key>
	<true/>
	<key>SUPublicDSAKeyFile</key>
	<string>dsa_pub.pem</string>
#endif
</dict>
</plist>

Note that after adding the preprocessor definitions, the file will no longer open as a property list. You’ll need to right-click on the item in the Groups & Files list and select Open As > Source Code File.

The MAC_APP_STORE preprocessor definition allows a different bundle identifier to be selected, forces 10.6.6 as a minimum OS version, and removes the Sparkle configuration information.

Distribution Targets

It’s now time to automate the build of these two targets. This will be done with two aggregate build targets. Create the following:

Distribution_Iconfactory
Distribution_App_Store

The first will be used for your own website and the second is for the App Store.

For both build targets, make sure to define the following build setting for the release configuration:

PRODUCT_NAME: Flare

This value gets used in the run scripts to generate file names.

Distribution_Iconfactory

The first thing in the “Distribution_Iconfactory” target should be “Flare”. After that, create a new run script build phase (right-click on “Distribution_Iconfactory” and select Add > New Build Phase > New Run Script Build Phase. Name it “Package Release”.

This script will create several things:

  • A ZIP file with build results. A version number is also added to the file name.
  • A signature for the file that Sparkle will use to verify a new update.
  • An appcast file that can be uploaded to your website.

The contents of the run script are as follows:

set -o errexit

[ $CONFIGURATION = Release ] || { echo Distribution target requires "'Release'" build configuration; false; }

VERSION=$(defaults read "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Info" CFBundleVersion)
DOWNLOAD_BASE_URL="http://iconfactory.s3.amazonaws.com/flare"
RELEASENOTES_URL="http://iconfactory.com/appcasts/Flare/release_notes.html#version-$VERSION"

PACKAGE_NAME=`echo "$PRODUCT_NAME" | sed "s/ /_/"`
ARCHIVE_FILENAME="$PACKAGE_NAME-$VERSION.zip"
DOWNLOAD_URL="$DOWNLOAD_BASE_URL/$ARCHIVE_FILENAME"
KEYCHAIN_PRIVKEY_NAME="Sparkle Private Key"

WD=$PWD
cd "$BUILT_PRODUCTS_DIR"
rm -f "$PRODUCT_NAME"*.zip
ditto -ck --keepParent "$PRODUCT_NAME.app" "$ARCHIVE_FILENAME"

SIZE=$(stat -f %z "$ARCHIVE_FILENAME")
PUBDATE=$(LC_TIME=en_US date +"%a, %d %b %G %T %z")

PRIVATE_KEY=$(security find-generic-password -g -s "$KEYCHAIN_PRIVKEY_NAME" 2>&1 1>/dev/null | /usr/bin/perl -pe '($_) = /"(.+)"/; s/\\012/\n/g' | /usr/bin/perl -MXML::LibXML -e 'print XML::LibXML->new()->parse_file("-")->findvalue(q(//string[preceding-sibling::key[1] = "NOTE"]))')
echo "$PRIVATE_KEY" > /tmp/sparkle.key

[ -n "$PRIVATE_KEY" ] || { echo Unable to load signing private key with name "'$KEYCHAIN_PRIVKEY_NAME'" from keychain; false; }

SIGNATURE=$(openssl dgst -sha1 -binary < "$ARCHIVE_FILENAME" | openssl dgst -dss1 -sign /tmp/sparkle.key | openssl enc -base64)

echo $SIGNATURE > /tmp/sig.out

rm /tmp/sparkle.key

[ -n "$SIGNATURE" ] || { echo Unable to create signature for "'$ARCHIVE_FILENAME'"; false; }

cat > "$BUILT_PRODUCTS_DIR/appcast.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"  xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Flare Release</title>
    <link>http://iconfactory.com/appcasts/Flare/appcast.xml</link>
    <description>Most recent version of Flare.</description>
    <language>en</language>
    <item>
      <title>Version $VERSION</title>
      <sparkle:releaseNotesLink>$RELEASENOTES_URL</sparkle:releaseNotesLink>
      <pubDate>$PUBDATE</pubDate>
      <enclosure
        url="$DOWNLOAD_URL"
        sparkle:version="$VERSION"
        type="application/octet-stream"
        length="$SIZE"
        sparkle:dsaSignature="$SIGNATURE"
      />
    </item>
  </channel>
</rss>
EOF

(Thanks to Marc Liyange for the original idea.)

Before this script will work, you need to create a secure note in your Keychain named “Sparkle Private Key”. Contents should be the “DSA PRIVATE KEY” text from the dsa_priv.pem file you created when setting up Sparkle.

Distribution_App_Store

The first thing in this aggregate target is “Flare App”. After that build step, add this run script build phase. Name it “Package Release”:

set -o errexit

[ $CONFIGURATION = Release ] || { echo Distribution target requires "'Release'" build configuration; false; }

VERSION=$(defaults read "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Info" CFBundleVersion)

PACKAGE_NAME=`echo "$PRODUCT_NAME" | sed "s/ /_/"`
ARCHIVE_FILENAME="$PACKAGE_NAME-$VERSION.pkg"

cd "$BUILT_PRODUCTS_DIR"
rm -f "$PACKAGE_NAME"*.pkg
productbuild --component "$PRODUCT_NAME.app" /Applications \
	--sign "3rd Party Mac Developer Installer: The Iconfactory" "$ARCHIVE_FILENAME"

This script produces a .pkg installer file that’s signed with the Iconfactory’s certificate.

Distribution Build Script

Since both of the distribution builds create a binary in your Project’s build/Release folder, you need to remember to clean the target before building. To make this easy, just create the following shell script named “build_release”:

#!/bin/sh

current_tools=`xcode-select -print-path`
build_tools="/Developer"

switch=0
if [ $current_tools != $build_tools ]; then
	switch=1
fi

if [ $switch -ne 0 ]; then
	echo "Switching from $current_tools to $build_tools..."
	sudo xcode-select -switch $build_tools
else
	echo "Using $build_tools..."
fi

configuration="Release"

target="Distribution_App_Store"
xcodebuild \
	-configuration "$configuration" \
	-target "$target" \
	clean build


target="Distribution_Iconfactory"
xcodebuild \
	-configuration "$configuration" \
	-target "$target" \
	clean build

if [ $switch -ne 0 ]; then
	echo "Restoring $current_tools..."
	sudo xcode-select -switch $current_tools
fi

This script also ensures that you’re building with a released version of the Xcode developer tools (installed in /Developer). This is important if you do iOS development, as you’re likely to have a beta versions installed as well.

Changes to App Delegate

So far, we’ve been focused on building and packaging your release. You’ll also need to change your source code to accommodate the distribution channel.

If you haven’t done so already, make sure that there are no Sparkle objects in your MainMenu.xib file (as is suggested by the documentation.) The reason for this is simple: the App Review team does a grep for SUUpdater on binaries are submitted. If you have an archived object in the NIB file you’ll be rejected.

Since you’ll still need an SUUpdater object, you’ll need to create an instance of the object manually as you awake from the NIB. Since the application delegate is also on the responder chain, it can also handle the -checkForUpdates: message:

#ifndef MAC_APP_STORE
#import "Sparkle/SUUpdater.h"
#endif

- (void)awakeFromNib
{
#ifdef MAC_APP_STORE
	[[checkForUpdatesMenuItem menu] removeItem:checkForUpdatesMenuItem];
#else
	[[SUUpdater alloc] init];
#endif
}

#ifndef MAC_APP_STORE
- (IBAction)checkForUpdates:(id)sender
{
	[[SUUpdater sharedUpdater] checkForUpdates:sender];
}
#endif

You’ll also need to create a NIB outlet for the “Check for Updates…” menu item so that it can be removed from the Mac App Store version.

(If you have your own purchasing and registration menu items, you can handle them in a similar manner. I can guarantee you won’t get through App Review with a menu item titled “Purchase Flare…”.)

There are two different bundle identifiers for a single application named “Flare” (one for the version from the website and another for the Mac App Store). A user could get confused if they have both copies installed (especially if one of them is a trial version with limited functionality.) To help users with multiple versions, you should add the following code at launch to check if the one from the web site is installed alongside another from the App Store:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
#ifndef DEBUG
#ifndef MAC_APP_STORE
	// check that a trial version isn't installed
	NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
	NSString *pathToAppStoreVersion = [workspace absolutePathForAppBundleWithIdentifier:@"com.artissoftware.flare"];
	if (pathToAppStoreVersion) {
		NSString *message = [NSString stringWithFormat:@"It looks like you have a copy of Flare from the Mac App Store installed in \"%@\".\n\nThe application you just launched is a trial version and should be removed. Would you like to quit now so you can move this unneeded file to the Trash?\n\nHint: Use the \"Move to Trash\" item in the \"File\" menu after the Finder window appears.", [pathToAppStoreVersion stringByDeletingLastPathComponent]];

		NSInteger result = [[NSAlert alertWithMessageText:@"Multiple Copies Installed" defaultButton:@"Quit and Reveal in Finder" alternateButton:@"Continue" otherButton:nil informativeTextWithFormat:message] runModal];
		if (result == NSAlertDefaultReturn) {
			[workspace selectFile:[[NSBundle mainBundle] bundlePath] inFileViewerRootedAtPath:nil];
			exit(0);
		}
	}
#endif
#endif

	…
}

If you’re using these techniques to build a beta version in parallel with your Mac App Store build, you’ll want to have some expiration checking code in the app. Otherwise, your final beta will find its way onto the Internet…

// expiration is used for any (beta) builds that aren't for the Mac App Store
#ifndef MAC_APP_STORE
#define USE_EXPIRATION 1
#else
#define USE_EXPIRATION 0
#endif
#if USE_EXPIRATION

- (void)checkExpiration
{
	NSDate *today = [NSDate date];
	
	// pick the date to expire on
	NSDate *expireDate = [NSDate dateWithString:@"2011-04-01 00:00:00 -0800"];
	
	NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init]  autorelease];
	[dateFormatter setDateFormat:@"h:mm a 'on' EEEE',' MMMM d',' yyyy"];
 	NSString *expireDateString = [dateFormatter stringFromDate:expireDate];

	if (! [[today laterDate:expireDate] isEqualToDate:expireDate]) {
		[[NSAlert alertWithMessageText:@"Take Five Beta" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@"This beta release of Take Five expired at %@.", expireDateString] runModal];
		[[NSApplication sharedApplication] terminate:self];
	}
	else {
		[[NSAlert alertWithMessageText:@"Take Five Beta" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@"This release of Take Five is a beta and will expire at %@.", expireDateString] runModal];
	}
}

#endif
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
…
	
#ifndef DEBUG
#if USE_EXPIRATION
	[self checkExpiration];
#endif
#endif
…
}

Changes to main.m

The license check for the Mac App Store should happen as early as possible in the app. It’s recommended that this happens before NSApplicationMain is called:

#import "ValidateReceipt.h"

int main(int argc, char *argv[])
{
#if MAC_APP_STORE
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	if (! validReceipt()) {
		pool = (NSAutoreleasePool *)1;
		exit(173);
	}
	[pool drain];
#endif

	return NSApplicationMain(argc, (const char **) argv);
}

The code that assigns an invalid value to the pool object is done as a simple protection against piracy. If someone tries to patch the exit(173), they’ll just be met with an objc_msgSend exception when the pool is drained.

Of course, you’ll want to add additional code that checks the validity of the receipt elsewhere in your code. The Apple Developer website has additional information on how to validate your store receipts.

Receipt Validation

As far as validating the license is concerned, the bulk of the code to parse and validate the receipt can be gotten from the ValidateStoreReceipt project on github. You’ll need to change the hard-coded bundle identifier, version numbers and paths used in the project as you adapt this code for your own product.

Gatekeeper

You’ll also want to sign code sign the app you distribute outside of the Mac App Store. Apple has a helpful guide on how to make your app compatible with Gatekeeper.

To quickly check that your build is signed correctly with a Developer ID, you can use the spctl command and make sure it is “accepted”:

$ spctl -a -v Flare.app
Flare.app: accepted
source=Developer ID

Testing

There’s a lot going on here, so you’ll certainly want to test both the distribution builds. The builds that are going to beta testers or onto your own website are straightforward: it’s just a ZIP file that can be loaded onto the target system. Once loaded, check that menu item for Sparkle updates is present. You’ll also want to check the Package Contents to see that the correct bundle identifier is set.

Things get a little more complicated when you start testing the build for the Mac App Store.

Start by reading Apple’s instructions on testing the installation process. Unfortunately, these instructions only show you how to get the application installed in your /Applications folder:

$ sudo installer -store -pkg Flare-1.0.pkg -target /

The documentation fails to mention that the installation process can fail if duplicate copies of the app are present elsewhere on the system (e.g. in the build folder.) Beta versions that use the same bundle identifier can also be a problem.

Apple’s test documentation also fails to mention that sandbox users accounts can be created in iTunes Connect (under Manage Users.) These user names and passwords can then be entered after launching the installed application and a receipt will be generated in the application’s _MASReceipt folder.

In order for this launch test to work, the app metadata must be defined in iTunes Connect (a binary does not need to be uploaded, it only needs to show up in “Manage Your Apps”.) Also make sure you sign out and quit the App Store app before launching your app to test the receipt processing code.

A bug report has been filed.

Conclusion

Hopefully these long-winded instructions will be helpful as you prepare your release for the Mac App Store. We’ve already used them several times for our own products, so I’m guessing they will be. :-)

iPhone multitasking

It’s no secret that “multitasking” is one of the great new features of iOS 4. Unfortunately, many people have a misconception about what Apple has implemented. Hopefully this short essay will help you understand the restrictions and the good reasons for having these limits.

On your desktop, multitasking means that any application or process can run at the same time as another. Technically, there’s no reason why the same can’t be done on a mobile device. However, from a more practical point-of-view, there is one good reason why you don’t want this: a running app uses energy that decreases your battery life. Running fewer apps means you can listen to music longer, make more phone calls, or call up Maps at the end of the day to find a place for dinner.

At the same time, there are certain types of app that must run in the background in order to be useful. Apple has identified three categories of apps:

  • Audio – Apps that plays audible content while in the background. The poster child for this type of application is Pandora.
  • VOIP – When you’re making or receiving phone calls over an Internet connection, you’re using a “Voice Over Internet Protocol” app that runs in the background. Skype is a good example.
  • Location – Some applications need to run in the background so they can keep track of your current location. An app that logs GPS coordinates while you take a walk or run would need to do this.

Additionally, apps can tell iOS 4 that they need additional time to complete a task or want the user to be notified at a specific time. This lets apps finish a long download or pop up the little blue window like an incoming SMS message.

These simple rules cover a wide variety of situations, but there are still some cases that aren’t covered. The main shortcoming is with apps that need to periodically refresh data. Social networking apps, chat programs, news readers, and other utilities that check the Internet for changing data don’t fit into any of the above categories. Developers have proposed solutions to these problems, but there’s no solution for today’s software.

Part of the confusion with multitasking comes from Apple’s excellent implementation of “task switching”. When you double-tap on the home button and start another application, the previous application is “frozen” and put into a state where it’s not running but can be restarted quickly. A part of the freezing process also reduces the amount of memory being used: allowing more applications to fit in freezer.

The next time you tap on the app’s icon, it is “thawed out” and put back on your iPhone’s screen while the previous app is frozen. This process is repeated each time you launch an app.

This sleight of hand makes it feel like you’re running many more applications than you actually are. It also explains how your iPhone can continue to have great battery life while you interact with many different apps. Most of your apps will be frozen and not using power: only the app on your screen is active. And even with audio, phone or GPS apps that are running in the background, you won’t be using more than one of those at a time (go ahead and try to listen to Pandora and the iPod apps at the same time!)

Hopefully this short explanation helps you understand that the developer of your favorite app isn’t being lazy about doing refreshing in the background! If you feel strongly about this situation, the best thing you can do is give Apple some feedback.

Updated July 1st, 2010: Matt Neuburg has written an in-depth explanation of fast app switching at TidBITS.

WORLD WILD DEBACHING CHOCK

DEAR LOSERS WHO ARE GOING TO WWDC HERES MY GUIDE TO MAKE YOUR TRIP PLEASENT IF YOU KNOW WHAT I MEAN

CHOCKTUB

APR 23 SO THIS GUY @MACGUITAR GETS IN THE HOT TUB AND SAYS HE WANTS TO SHOW ME HIS THINGS WHAT A WEIRDO MADE @GRUBER JEALOUS TO

CHOCKTUB

APR 24 @jsnell BE CAREFUL OR I WILL MAKE YOU GET IN THE HOT TUB WHAT KIND OF WHINE DO YOU LIKE

CHOCKTUB3

MAY 8 WELL LOOK WHOS SHOWN UP AND HE BROUGHT SOME READING MATERIAL COULD IT GET ANY HOTTER IN THIS TUB I HOPE SO

CHOCKTUB4

MAY 9 NOW THAT @SIRACUSA HAS FINALLY SHUTUP ABOUT HIS DEEP FEELINGS FOR LARRY WALL WERE GOING TO LEARN ABOUT THE BLINK TAG FROM JEFFRAY PARTY ON

CHOCKTUB5

MAY 13 OK WHO LET THE CHICKS IN I TELL YOU WHAT IF SHE STARTS UP WITH THE POETRY SHELL BE LOOKING FOR A WORD THAT RHYMES WITH PALM

CHOCKTUB6

MAY 16 WHO THE HELL LET THE KID INTO THE HOT TUB BOY BE CAREFUL OF THE GUY BEHIND YOU HELL WANT TO SHOW YOU HIS THINGS YOU CAN LEAVE YOUR HAT ON TO

CHOCKTUB

MAY 19 I THOUGHT CATS DIDNT LIKE WATER THAT SNEEKY BASTARD @SOCKINGTON IS PROBABLY GOING AFTER MY WHINE

CHOCKTUB

May 21st Looks like Twitter’s avatar uploading is broken. Again.

MAY 21 OR MAYBE IT WAS JUST THAT @ATEBITS AND ME BEING IN THE SAME TUB WAS TO HOTT TO HANDLE

MAY 21 IN MORE IMPORTANT NEWS TWITTER FIXED THERE SERVER SO THE CACTUS FITS IN THE HOTT TUB AND @GRUBER THOUGHT SITTING IN MY LAP WAS PRICKLY

CHOCKTUB

MAY 29 SO ITS BIKINIS ON MONDAY AND WOLFS SHIRTS ON TUESDAYS WHAT ARE WE GOING TO WEAR THE REST OF THE WWDC WHEN WERE NOT IN THE HOTT TUB

MAY 29 AN IMPORTANT AVATER UPDATE IS NOT WORKING AND NO IM NOT WEARING A BIKINI IN IT BUT ITS BETTER

MAY 31 HEY HEY IT LOOKS LIKE THAT FOXY @TJW DUDE LOST HIS HAT ON THE WAY TO THE HOTT TUB AND WHAT THE HELL DOES INMO MEAN http://twitpic.com/59r88

CHOCKTUB

JUN 3 WHO LET THE DOGS OUT THIS HOTT TUBB IS FULL OF PARTY ANIMALS NOW MOOF MOOF MOOF MOOF http://bit.ly/QNz2G AND DONT SPILL MY WHINE

CHOCKTUB

JUN 4 I LOVE TO CUDDLE WITH TEDDY BEARS ESPECIALLY IN THE HOTT TUBB WONDER WHO LET HIM OFF THE FARM IN THAT KEEWEE COUNTRY

CHOCKTUB

JUN 4 OH CRAP ANOTHER BEAR SHOWED UP BUT THIS ONES CANADAIAN AND HES GOT HIS EYES ON MY DRINK HES ALMOST A FRENCHIE TO

CHOCKTUB

JUN 5 WHOA SOMEONE AND THERE ROCK HARD ASS JUST SHOWED UP IN THE HOTT TUBB NEVER SEEN SUCH FANCY SWIM TRUNKS EITHER WHAT AN PARTY ANIMAL

CHOCKTUB

JUN 5 THE HOTT TUBB IS GETTING SO FULL WERE ONLY ALLOWING SUBATOMIC PARTICLES FROM THE HARDON COLLIDER MAYBE HIS EXISTENCE PROOFS THE THEORY

CHOCKTUB

JUN 5 @Curvyboom YOU CHICKS SPEND ALL YOUR TIME ON FACEBOOK PLANNING LUNCH AND WAITING IN LINE FOR THE TOILET NO TIME TO PARTY WITH US ANIMALS

JUN 6 WITH ALL THE CHICKS SHOWING UP FOR WWDC I KNEW ONE OF THEM WOULD TRY TO SNEAK INTO THE HOTT TUBB GOOTEN MORGEN MINE FRAULINE ICH HABEN CHOCK

CHOCKTUB

JUN 6 IT LOOKS LIKE SOMEONE WITH A TELETYPE SHOWED UP TO THE HOTT TUBB ID KICK HIM OUT BUT HIS PALMS LOOK DELICIOUSLY FLESHY

CHOCKTUB

JUN 6 @dsandler ITS EASY TO EXPLAIN JUST SAY THAT ITS PINK AND WARM

JUN 7 ANOTHER DAY ANOTHER CHICK IN THE HOTT TUBB SHES ALMOST AS PINK AS I AM AND I THINK SHES LAUGHING AT US NOT WITH US WONDER IF SHES MARRIED

CHOCKTUB

JUN 7 @AmyJane ILL MAKE SURE TO WHISPER POPS IN HIS EAR WHEN WE HAVE OUR ANUAL SNUGGLE

JUN 8 YOU LOSERS WHO DIDNT COME OVER TO THE YERBO BUENO HOTT TUBB MISSED OUR SURPRIZE GUEST DURING THE KEYNOTE @GRUBER WAS SO EXCITED HE PEED

CHOCKTUB

JUN 9 ITS GOT SO CROWDED IN THE HOTT TUBB LAST NIGHT I HAD SOME GOLDEN GATES INSTALLED SO YOU CAN ALL LEAVE NOW

CHOCKTUB

JUN 10 SOMEONE TURNED UP THE HEAT ON THE HOTT TUBB LAST NIGHT AND IM STILL FEELING A BIT WOOZY ALSO SEEING GRUBER SWEAT IS NASTY

JUN 10 OOOOOOHHH FUZZY BALLS ARE TICKLING MY NOSE IN THE HOTT TUBB AFTER FREE BEER AT THE MAC WORLD

CHOCKTUB

JUN 11 YEAH ITS TRUE I MAY BE GETTING OUT OF THE HOTT TUBB TO PERFORM AT A BASH WORKING OUT DETAILS WITH THE LAWYERS

JUN 11 FRICKEN LAWYERS WONT MEAT MY DEMANDS FOR THE YERBO BUENO SHOW MAY HAVE TO GET A FAMILY MEMBER TO DO IT STAY TUNED FOR OFFICIAL CHOCK ROCK

JUN 11 BTW DONT FOR GET TO USE CHOCKROCK WHEN LOCALIZING FOR JAPAN

JUN 11 IF YOUR LOOKING FOR THE HOTT TUBB ITS CONVENIENTLY LOCATED NEAR THE FOUNTAIN AND KEGS LOOK FOR GRUBER IN HOTT PANTS

JUN 11 YES I AM LIVE TWEETING THE BASH TURN OFF YOUR FRICKEN PHONE SO I GET SIGNAL LOOSER

JUN 11 HOTT TUBB http://yfrog.com/5hgdgtj

HOTT TUBB

JUN 11 CAKE IS BEING SERVED IN THE HOTT TUBB BEER TO

June 12th For me, there will never be a beer bash as special as the one this evening.

JUN 24 AND YOU THOUGHT I WAS JOKING ABOUT MY BUDDY STEVE BEING IN THE HOTT TUBB http://bit.ly/Y8MCf A MAGIC MOMENT

Don’t design for early adopters

If you’re like me, the iPad has changed how you look at computers in just a matter of weeks. The possibilities for this device seem endless. It’s natural at this point to start thinking about the future, and to do that thinking in terms of the past.

As an example, we’ve been getting plenty of feature requests for Twitterrific that ask for features and capabilities that exist in other mobile and desktop software. That’s not surprising, since one of our early decisions with the iPad app was to trim the app down to its bare essentials.

Part of this had to do with schedule constraints. Sixty days was not a lot of time to build a product from scratch.

But a more important reason for paring back the design was to simplify the user interface. A new kind of user is about to be introduced to a computer that they can actually use: less interface is more as far as they’re concerned. We designed our iPad app with our families in mind, not the Twitter power user.

It turns out that our design intuition was pretty good:

The non-tech-savvy users want something simple, to push an icon and get your e-mail and go online. With the iPad, people don’t feel intimidated by it.

Mary Elizabeth O’Conner

To this technical-ninny it’s clear
In my compromised 100th year,
    That to read and to write
    Are again within sight

Of this Apple iPad pioneer.

Virginia

Even folks that have access to the latest and greatest technology are preferring the iPad over more complex devices. Initial statistics also show that the iPad has an older demographic.

Of course, 300,000 geeks like you and me don’t fall into that category. We’re the first ones standing in line at the Apple store, and the first ones to use all this cool new software. And we know all the things that apps “used to do”. And we want all sorts of other bells and whistles. And we’re wrong.

As a developer, you should be very careful about this early feedback for your app. Simplicity is the name of the game in this new world order. You don’t maintain simplicity by adding tons of features.

Two of the most requested features after the 1.0 release of Twitterrific for iPad were Instapaper support and photo uploading. If you think about these things, both have a high cognitive load. To use Instapaper, you need to know that:

  • The service exists, and that you can signup for an account.
  • That you can install a bookmark and use it to save pages from your browser.
  • That these saved pages can be synced to your iPad using another application.

For photo uploading, you have to know how to do one of these things in order to “get a picture”:

  • Use the camera connection kit to transfer photos, or
  • Take a screenshot with a non-intuitive gesture (the power and home buttons), or
  • Tap and hold on a picture in a web page and save it to an album, or
  • Create events in iPhoto and sync them with iTunes

Ask yourself if you could explain any one of these things to your mother in less than 25 words. I know I can’t and I’m pretty good with this kind of stuff.

Of course, these are both useful features and we added both of them in subsequent releases. In the case of Instapaper, the feature doesn’t even show up in the UI until you go into the Settings app and add your account information. My mom will never see it.

Photo uploading is a simple button on the compose screen that lets you choose a photo from your library. Eventually, our mothers will figure out how to add things to the photo library and use that feature. But they’ll be happiest when a future device let’s us put a “Take Picture Now” button on that screen.

It’s very easy to get caught up in the excitement this new device has generated in the last month and a half, but the real thrill will be in a year’s time when people who’ve never used a computer will be telling you how much they love your app. And there will be a lot more than 300,000 of them…

Updated: Check out similar thoughts from my colleages: David Lanham and Gedeon Maheux.

A lot of typing

I’ve always wondered what it would be like to write a book. Now I know. It’s the hardest thing I’ve ever done, but rewarding beyond words.

For those of you asking for PDF or Ebook editions, you can order them now directly from O’Reilly. The printed version will be available in a couple of weeks. The “Look Inside” feature at Amazon.com will give you an idea of what the book covers. I’ve also written an overview of each chapter. (And for those of you who are dying to know, the CHOCKLOCK first appears on page ix.)

If you think you know everything about iPhone development, this book will probably prove you wrong. I learned a lot while writing it, and it’s my sincere hope that you’ll benefit from reading it.