Reading Time: 5 minutes
In this post I’ll explore some of the variations in app version numbering we’ve tried in past projects and explain which one we eventually standardized on.
TL;DR
This is our recommended scheme for version numbers:
- User facing version numbers (CFBundleShortVersionString/ versionName) should follow semantic versioning.
- System version numbers (CFBundleVersion/versionCode) should be incremental integers.
In the rest of this post I’ll give some background and pros and cons of alternative approaches.
User facing version numbers
CFBundleVersionShort (iOS) and versionName (Android) are the user facing version numbers and we recommend they follow Semantic Versioning. This means an example version could be 2.0.1, where 2 is the major version number (major new features, redesign), 0 is the minor version number (a release with a few new features and some bug fixes) and 1 is the patch level (bugfixes only).
SemVer has been around for ages, is widely accepted and easy to implement. Users generally don’t care about the version number but for developers and product owners it makes it easier to keep track and/or remember what went into a particular version.
We never really considered any alternatives here. We did experiment a bit with formatting and decided to format them with a minimum of two levels and with suppressing the third level of its zero. In other words, we would say 2.1, but not 2.1.0. If there’s never a patch, it keeps the number nice and short. The third level only appears with the first patch, so we go from 2.0 to 2.0.1.
Internal version numbers
CFBundleVersion (iOS) and versionCode (Android) are the version numbers used by the operating system. Both iOS and Android require that this number should be higher than the previous for every new submit, and you can skip numbers if you want to.
Android
For Android there’s not much discussion, Android requires this number to be an integer. This means two options:
- It can be an incremental integer (1, 2, 3, 4, 7) with no relationship to the user facing version.
- It can be an int and still have a relation to the user facing version. A naive approach would be to translate version 2.0.1 into the int 201, but this gets you in trouble when you release a version 2.11. A better approach is the one proposed by Erik Ogenvik from Jayway: 2.0.1 would become 2.00.01, thus 20001, meaning you can have up to 99 versions and 99 patch levels.
We chose the first approach as it’s easier to automate in a continuous integration environment (we use Buildozer and it can put the build number into the versionCode field). If you use CI and want to use the second approach, check out the jayway post as it has suggestions for appending a ‘-snapshot’ for each individual build.
iOS
For iOS we had more options:
- Fill CFBundleVersion with an integer, like with Android. Clean and simple, easy to automate, but again no relationship to the user facing version.
- Fill it with a commit hash to tie the build to a specific change in your repository. This used to be possible before Apple integrated TestFlight, but since they now require that each new value is higher than the previous this is no longer an option.
- Make it equal to the user facing version, e.g. 2.0.1. This works fine until you have to re-upload a build for the same release to iTunesConnect. You would run into a ‘there’s already a build with this version’ error. You could then bump the version to 2.0.2 but your users would never see 2.0.1, and this felt wrong. So when this happened we would add a fourth level: 2.0.1.1. You can’t put a four digit number in the user facing version (Apple doesn’t allow it), so for the user it would still be 2.0.1 but as a developer you would know it was a second upload.
- Use 2.0.1. by default.
- Use the Jayway (20001 for 2.0.1) approach.
When we standardized the versioning at Egeniq we also asked ourselves: ‘What would Apple do?’ My colleague Johan Kool dug up some examples:
- Mail 9.3 (3124)
- System Preferences 14.0 (14.0)
- Photos 1.5 (370.42.0)
- OS X 10.11.4 (15E65)
- Safari 9.1 (11601.5.17.1)
So no standardization among Apple teams it seems, they use all of the above methods and then some.
In the end we democratically decided with a vote on Slack that the relationship between the two numbers has little to no significance since you can always see both numbers. So we settled for method 1 (an incremental int), clean and simple. It also means we can use the same scheme across Android and iOS, and it was easy to have our build system automatically add the number.
One caveat: once you select a versioning scheme you may have to stick with it forever. You can’t go from 1.2.0.1 to and integer because Apples App Store verification scripts would always trigger the ‘not bigger than the previous’ error. This means for new projects decide on a scheme before you submit the first build, or better: standardize this across all your projects like we did.
I hope that by providing some background to the version numbering scheme we use at Egeniq, I have provided some input for your own choices. Whatever method you choose, having some consistency is probably more important than the actual choice.