Matt Gallagher deserves a medal…

Every once in awhile you read a blog post that completely changes the way you think about a problem. Matt Gallagher’s Cocoa With Love is one of those blogs where it happens often. If you’re not subscribing to his RSS feed, do it now.

In particular, this post addressed a problem that every iPhone developer has encountered: any kind of settings or form UI is a pain in the butt if you follow Apple’s sample code. And, of course, this is exacerbated because these essential interfaces aren’t very fun to code. For both Twitterrific and Frenzic, it’s been a frustrating experience: I wanted to fix how this standard UI was created and maintained.

The motivation

Before I start talking about what I did using Matt’s concepts and code, I’d like to discuss the need for settings in an application.

There are some people who think that settings should only be in the Settings app. There are others that think they should only be within the application. I can honestly see how both groups are right, but what both sides fail to realize is that it’s often a matter of context.

If you’re working on a simple application with simple needs, relying on the infrastructure provided by the SDK is fine. You may have some additional support load for users who have problems finding your settings, but that’s a reasonable tradeoff for saving development time. Sophia Teutschler’s Groceries app is a fine example of where the built-in settings shine: I turned off the Marker Felt font several months ago, and I haven’t touched it since.

In the case of Twitterrific, there were three main reasons why we built the settings into the application:

  1. We could not split the settings between two locations. From a user’s point-of-view, it’s incredibly confusing to have an application’s configuration in two places. This would have happened if we had put the account settings in the application and everything else in the Settings app.
  2. Some of the settings are things that people will change on a fairly frequent basis. For example, the dark theme works best at night, while a light theme is better during the day. Leaving the app to make these types of adjustments is inconvenient.
  3. The Settings app can’t handle preferences that are “dynamic.” An example is a vibration setting for the notification: there’s no way to make this appear on an iPhone but not on an iPod touch.
As we start to see more complex applications appearing on the App Store, I think there will be a lot of other developers coming to grips with settings in their applications. That’s where my code comes in…

The solution

I’ve taken the basic concepts that Matt presented in his article and extended them to create a pixel perfect replica of what you see in the Settings application. Instead of configuring applications using a property list, you do it with some very simple code in a view controller. (Domain-specific languages, such as those found in Ruby, were an inspiration for the methods used in this code.)

And if you’re not doing settings, the classes are still very handy for making user input forms. They were used in all of the search forms used in the new version of Twitterrific.

To give you a quick taste of how it’s used, here’s a complete implementation of a table view controller that presents a single group with a text field and a switch control. In just six lines of code:

- (void)constructTableGroups
{
  NSMutableArray *cells = [NSMutableArray array];
  IFTextCellController *textCell = [[[IFTextCellController alloc] initWithLabel:@"Text" andPlaceholder:@"Placeholder" atKey:@"sampleText" inModel:model] autorelease];
  [cells addObject:textCell];
  IFSwitchCellController *switchCell = [[[IFSwitchCellController alloc] initWithLabel:@"Switch" atKey:@"sampleSwitch" inModel:model] autorelease];
  [cells addObject:switchCell];
  tableGroups = [[NSArray arrayWithObject:cells] retain];
}

The real beauty of this code is that it’s incredibly easy to change. Settings and forms tend to evolve over the lifetime of a project, so using these simple declarations for the table view layouts makes it much simpler to adapt to new requirements. If I need to change the switch control shown above into a choice list, I can do it in a couple of lines of code.

So without further ado, here’s the source code in a sample project. The code is well documented, so you shouldn’t have any problems figuring out how it all works. Start by looking at the RootViewController, then take a look at the SampleViewController, followed by the SampleAdvancedViewController. If you’d like to incorporate this code into your own project, just grab all the classes with the Iconfactory prefix (“IF”.)

The license

Previously, I’ve released source code on this site without any licensing at all. This time, I’m going to try something new. You can do anything you want with this code with one requirement: you need to give the Iconfactory some link love in your product.

For more details on the licensing terms, check out the Licensing.rtf file that’s included in the sample project. If you can’t abide by this restriction, please get in contact and we can try to work something out.

I hope this code saves you as much time as it has saved me. Enjoy!

A thought experiment

Assume the following:

  • You have an application that you’re selling on the App Store. This application, MyApp 1.0, works on both iPhones and iPod touches with the 2.2.1 firmware.
  • The compelling new APIs in iPhone SDK 3.0 allow you to implement a bunch of great new features in your product. Let’s say you add a Map View and release MyApp 2.0 (after SDK 3.0 is released.)

Now, what happens if you find a bug in MyApp? Let’s say it’s a simple thing like dereferencing a nil pointer that causes a crash: something that can be fixed with a single line of code. You easily fix this bug, but you can’t give this fix to all of your customers. Why?

The problem lies with iTunes Connect. It only allows you to upload a single binary. And that single binary is specified to work with a single version of the iPhone firmware. Even if you have branching tools in your version control system, you can’t use them to produce an update for both versions of the SDK.

This presents a problem for customers who are still running the 2.2.1 firmware: they can’t get your fix until they upgrade to the 3.0 firmware.

Despite the fact that there will be a lot of uptake on this new release and all its great new features, it still feels wrong that an iPod touch owner will need buy the update in order to get my fix. I don’t like it when customers have to pay for my stupidity.

If you agree, please dupe Radar ID 6735814. Thanks!

Slow ride, make it easy

Many of us are developing iPhone applications running in a simulator connected to a very fast Internet connection. Too bad the customers of these applications won’t be using the same environment.

It’s very important to be able to profile and debug your application while it’s running on a slow network. You’ll find lots of weird timing problems, bad connection error handling, timeouts that are too short, and other things that are likely to occur in real world conditions.

Initially, I did testing for Twitterrific by loading the latest version onto the device and then walking around outside away from my office Wi-Fi network. That was a good first step, but also one that didn’t allow me to run gdb or Instruments when a problem occurred.

And then one day it hit me: the iPhone Simulator is running on top of the Mac OS X network stack. An environment that I can manage with standard Unix command line tools. Here’s the result:

#!/bin/bash

# configuration
host1="twitter.com"
host2="search.twitter.com"

# usage
if [ "$*" == "" ]; then
  echo "usage: $0 [full|fast|medium|slow|wwdc|off]"
  exit
fi

# remove any previous firewall rules
sudo ipfw list 100 > /dev/null 2>&1
if [ $? -eq 0 ]; then
  sudo ipfw delete 100 > /dev/null 2>&1
fi
sudo ipfw list 110 > /dev/null 2>&1
if [ $? -eq 0 ]; then
  sudo ipfw delete 110 > /dev/null 2>&1
fi
sudo ipfw list 200 > /dev/null 2>&1
if [ $? -eq 0 ]; then
  sudo ipfw delete 200 > /dev/null 2>&1
fi
sudo ipfw list 210 > /dev/null 2>&1
if [ $? -eq 0 ]; then
  sudo ipfw delete 210 > /dev/null 2>&1
fi

# process the command line option
if [ "$1" == "full" ]; then
  echo "full speed"
elif [ "$1" == "off" ]; then
  # add rules to deny any connections to configured host
  if [ -n "$host1" ]; then
    sudo ipfw add 100 deny tcp from $host1 to me
    sudo ipfw add 110 deny tcp from me to $host1
  fi
  if [ -n "$host2" ]; then
    sudo ipfw add 200 deny tcp from $host2 to me
    sudo ipfw add 210 deny tcp from me to $host2
  fi
else
  # create a pipe with limited bandwidth
  bandwidth="100Kbit"
  if [ "$1" == "fast" ]; then
    bandwidth="300Kbit"
  elif [ "$1" == "slow" ]; then
    bandwidth="10Kbit"
  elif [ "$1" == "wwdc" ]; then
    bandwidth="1Kbit"
  fi
  sudo ipfw pipe 1 config bw $bandwidth

  # add rules to use bandwidth limited pipe
  if [ -n "$host1" ]; then
    sudo ipfw add 100 pipe 1 tcp from $host1 to me
    sudo ipfw add 110 pipe 1 tcp from me to $host1
  fi
  if [ -n "$host2" ]; then
    sudo ipfw add 200 pipe 1 tcp from $host2 to me
    sudo ipfw add 210 pipe 1 tcp from me to $host2
  fi
fi

sudo ipfw list

You’ll notice a couple of configuration parameters: host1 and host2. Unless you’re one of the hundreds of Twitter client developers, you’ll probably want to change those values.

Turning a connection “off” can be used to simulate a site being offline. Using the “wwdc” setting allows you to relive those exciting moments of waiting in line with thousands of other geeks banging on the SOMA EDGE network. To turn off the bandwidth limit, use the “full” setting. The other values can be used to alter the quality of the connection in the simulator and make it feel more like it’s on a cellular network.

(I actually uncovered a bug in the beta version of Twitterrific while waiting in line for the WWDC 2008 keynote, so don’t think that setting is purely a joke.)

This bandwidth trick can be extended for device testing. On your development machine, turn on Internet Sharing (in System Preferences > Sharing.) Then use your device to connect to that shared network. Any ipfw rules you set will affect the device since all of its packets pass through the gateway you’ve established on your machine.

Another trick you can use to test your networking code is by opening the Networking preference panel and making your outbound connection inactive. Since the iPhone Simulator’s networking code sits on top of the System Configuration framework, any state changes will be passed onto your app running in the simulator. (If you’re using the Reachability class, this will make sense.)

And as I mentioned above, being able to do all these things from the comfort of your office chair and Xcode debugging environment has a lot of advantages. So slow down and do some real testing!

Updated March 25th, 2009: Mike Schrag has written a nice System Preference panel called Speed Limit which does the same thing in a nice GUI. You really have no excuse now :-)

Open sesame

Here’s a simple little script that saves me a lot of time:

#!/bin/sh 

if [ -z "$1" ]; then
  echo "usage: $0  [ Preferences |  ]"
else
  base=~/Library/Application\ Support/iPhone\ Simulator/
  apps=Applications
  app=`ls -1td "$base/"*"/$apps/"*"/$1.app" | head -1`
  if [ -n "$app" ]; then
    dir=`dirname "$app"`
    if [ "$2" = "Preferences" ]; then
      open "$dir/Library/Preferences"
    else
      open "$dir/Documents/$2"
    fi
  fi
fi

Put this script somewhere on your PATH and name it opensim. Use it to find the most recent application loaded in the simulator and open the Preferences, Documents or a named document in the Finder.

For example, to open the current Documents folder for an application in the iPhone Simulator, just do this:

$ opensim Thunderbird

To open a document within that folder, you can do this:

$ opensim Thunderbird chockenberry.db
$ opensim Thunderbird accounts.plist

You can easily get to your preferences folder with:

$ opensim Thunderbird Preferences

Notice how I haven’t mentioned a GUID in this essay? That’s the feature…

Sharing iPhone projects

The latest version of Xcode has a “feature” that prevents you from specifying a wildcard name for the Code Signing Identity. (This feature does make it easier for new developers or people working alone, so I’m not going to write a Radar for this behavior.)

When you have multiple people working on an iPhone project, this behavior is very annoying because every developer has to change the Code Signing Identity before they can run the latest version on the device. Here’s a typical scenario:

  1. I set the Code Signing Identity to “iPhone Developer: Craig Hockenberry” and do my device build. Everything is wonderful and I check in a new SekretApp.xcodeproj/project.pbxproj.
  2. I add some cool new features to SekretApp and I want Anthony Piraino to try them out. Anthony updates his working copy to the latest version.
  3. Anthony has to change the Code Signing Identity to “iPhone Developer: Anthony Piraino” so that he can do the device build. He does, and everything is wonderful.
  4. Anthony then adds some new files to the project. He checks in a new SekretApp.xcodeproj/project.pbxproj as a result of this change.
  5. I need these new files, so I get the latest version of the project. Unfortunately, this latest version has Anthony’s Code Signing Identity in it. Everything is not wonderful.
  6. Lather, rinse, repeat.
It gets even more fun when you have merge conflicts. The solution to this problem is to get rid of the individual developer names in the project.pbxproj file.

Before you begin, make sure that you’ve closed the Xcode project you’re going to be updating. You are going to be modifying files used directly by Xcode, so things can get screwed up if you make changes while the project is alive.

The first step is to locate SekretApp.xcodeproj in the Finder and right click on the file. Select “Show Package Contents” to reveal the project.pbxproj file.

Now, open project.pbxproj with your favorite text editor and look for the Code Signing Identity. It will look something like this:

CODE_SIGN_IDENTITY = "iPhone Developer: Craig Hockenberry";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Craig Hockenberry";

Change the identity to look like this:

CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";

You also need to delete the embedded provisioning profiles. These are normally located a few lines below the CODE_SIGNING_IDENTITY:

PROVISIONING_PROFILE = "DEADBEEF-1337-FACE-F00D-EA7A7BADCAFE";
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "DEADBEEF-1337-FACE-F00D-EA7A7BADCAFE";

(And, yes, I felt very lucky the day I was assigned that GUID.)

Note that there will probably be more than one CODE_SIGNING_IDENTITY (depending on your Project and Target settings.) Make sure to search through the whole file.

After making these changes, re-open the project in Xcode, clean it, and do a device build. If you make it through the CodeSign build phase, you’re good to go. Now all you have to do is check in the new version of project.pbxproj.

This trick works because the underlying codesign utility searches your keychain for a match. From the SIGNING IDENTITIES section of the manual page:

To be used for code signing, a digital identity must be stored in a key-chain that is on the calling user’s keychain search list. The identity is located by searching all such keychains for a certificate whose subject contains the identity string given. If there are multiple matches, the invocation fails and no signing is performed.

Since you and everyone else on your team has only one certificate with the pattern “iPhone Developer”, this search works correctly and no one will need to update the project settings after they check out the latest version of project.pbxproj.

And everything will stay fricken’ wonderful.