If you have developed software in Xcode and submitted it to either the iOS App Store or Mac App Store I'm guessing you have run into at least a few rejections because your versioning scheme was wrong or was not manually updated before creating the Archive.

At the very least the two different version fields must of caused some confusion. I will do my best to clarify what is required and offer some automatic tools to prevent (hopefully) future headaches.

Version vs Build

  • Version: This is the public version. That is, when you submit your app and people view it on the App Store this is what they will see. It's a good idea (but not required) that this be a semantic version. Also known as the short version or CFBundleShortVersionString.
  • Build: This is the private version. That is, it can be used internally to identify beta or other prerelease builds. Once again you not restricted here (actually you are, it's wildly frustrating, but I'll get to that in a moment ) and most people use an ever increasing integer that does not reset with new releases - more commonly known as a build number. Also known as CFBundleVersion.

To submit a build both of these must be greater than the last build submitted. Even if one or more of the last builds were never released (such as for beta testers). This is kind of a pain because while increasing the Build is no big deal you will also have to increase the patch number on the Version.

According to the Apple Documentation these versions can contain any characters. This is fantastic news if your working in an environment where you have several long-running features that need to be developed or tested at the same time; you can reflect this in the Build, like: 1.2.3-myfeature45 and 1.2.3-myotherfeature3.

Unfortunately not. This is because iTunes Connect does version comparisons on uploaded builds and will reject anything that actually sits outside of the dot-separated-integer build numbers. Although this was very hard to debug because it doesn't provide you with this information in the error response. Damn.

Let's Play It Apple's Way

So let's just pretend that I didn't spend a couple of hours trying every combination of versions to find out what iTunes Connect will or won't allow me to do and just assume that I want to use the most common Apple versioning scheme. The one described above.

First, we should automatically increment the Build with each actual build. Makes sense, right? To do this we use shell scripts attached to the build process.

Open up the scheme for editing (this is the build configuration). Edit Scheme... in the image to the left.

Important: If you are sharing this project with others (via source control) or running it on any external computer (including a CI system) you must check the box that saves your Scheme with the project otherwise other copies of this project will not include these scripts.

Click on Build > Post-actions.

We want to increment the build number after the build because we'd rather not increment the build if it wasn't successful - such as a compile error.

Click on the + button at the bottom and add a New Run Script Action.

Important: Xcode will not fail your build if this script fails (it will ignore the exit status of any scripts we attach to the scheme). Furthermore all errors and standard out will be totally suppressed so debugging these scripts can be near impossible.

Using the following code:

LAST_NUMBER=$($PLB -c "Print CFBundleVersion" "$PLIST")
$PLB -c "Set :CFBundleVersion $NEW_VERSION" "$PLIST"

Important: Make sure you select your target above (by default it may not be selected) which will also break your script and you will have no idea why.

Save your configuration and do a few successful builds and you should see your Build incrementing. If so, proceed.

Taking It One Step Further

To have the Build number increment automatically is nice but that's only one of the two version numbers we need to increase. It would be great if the patch was increased (1.2.3 -> 1.2.4) automatically when an Archive build is made.

Well, I'm sorry to say the similar technique above does not work for Archive builds as you would expect. Not because the version is in a different format, I had some fancy bash to handle that ‡, but because the time at which the build process captures the current version and when it increments it can never be done in a way that:

  1. The patch is incremented.
  2. The Archive build contains the new patch version (so it's correct).
  3. We can tag the correct patch version in git. I'll explain this next.

I'm happy to be proven wrong. Perhaps I missed a combination that just works - please let me know if you figure it out.

So... I guess the cat's out of the bag. I mentioned git tagging. Not only because it's a good idea but if you have many concurrent features being developed it's still hard to match up versions and build numbers with what that Archive actually contains.

The easiest and most reliable way is to tag the commit responsible for the build with the build number and/or version. But who want's to do all that work? Well, your computer does.

Using the same process above to go into edit the scheme and we will add a new script to Archive > Pre-process:

LAST_NUMBER=$($PLB -c "Print CFBundleVersion" "$PLIST")
$PLB -c "Set :CFBundleVersion $NEW_VERSION" "$PLIST"

The main point of this script is to undo the build number caused by the previous Build phase so that when we do an Archive build the number used in the archive is not out of whack with the correct build number in the version control (and the soon to be tag).

Another script is added to the Archive > Post-process:

SHORT_VERSION=$($PLB -c "Print CFBundleShortVersionString" "$PLIST")
BUILD_NUMBER=$($PLB -c "Print CFBundleVersion" "$PLIST")

# Tag version.

The git tag will look something like v1.2.3-build456. Feel free to customise this however you like.

The git tag will not be pushed to a central repo automatically. I left that out because it would make the archive much slower, cause it to depend on an internet connection and not everyones local git client is configured with all the right credentials.

Important: Make sure you select the target on both scripts. I can't stress this enough. If you don't, it just plain won't work and you'l be ripping your hair out trying to work out why.

‡ This was the fancy bash I originally had that I threw away:

# The location of the .plist that contains the target version properties.

# The PlistBuddy tool is used to extract and store Plist values.

# The Xcode "Version" is the public semantic version like '1.2.3'. You increment
# this number when appropriate.
SHORT_VERSION=$($PLB -c "Print CFBundleShortVersionString" "$PLIST")

# The Xcode "Build" contains the "Version" with branch and build suffix, like
# '1.2.3-branch4'. The build number ('4') will be incremented with each build.
VERSION=$($PLB -c "Print CFBundleVersion" "$PLIST")

# Exract the '4' from '1.2.3-branch4' and increment it.
LAST_NUMBER=$(echo $VERSION | grep -oE '[0-9]+$')

# Get the last word of the git branch as the identifier.
BRANCH=$(cd ${PROJECT_DIR}; git symbolic-ref -q HEAD)

# Rebuild the new "Build" and save.
$PLB -c "Set :CFBundleVersion $NEW_VERSION" “$PLIST"