In a React Native iOS app, how do CFBundleVersion and CFBundleShortVersionString differ, and which must change on every App Store Connect upload?
answer
- build number vs marketing version
- Info.plist reads two build settings
- CURRENT_PROJECT_VERSION and MARKETING_VERSION
- uploads need an unused build number
- users see only the short version
basics
~10 sCFBundleShortVersionString is the user-facing release version, such as 1.4.0; CFBundleVersion is the build number. Each App Store Connect upload needs a build number not used before for that version, so it changes every upload.
solid answer
~40 sIn a React Native iOS project, `Info.plist` sets `CFBundleShortVersionString` to `$(MARKETING_VERSION)` and `CFBundleVersion` to `$(CURRENT_PROJECT_VERSION)`, so you edit them as build settings (**Version** and **Build** in the target's General tab); a fresh project starts at `1.0` and `1`. The **short version** is the marketing version users see in the App Store. The **build number** identifies one binary of that version: App Store Connect rejects an upload whose build number was already used for that version, so every re-upload, even of an unchanged version after a rejected build, needs a new, higher build number. Teams keep the build number strictly increasing, often from a pipeline counter. From JavaScript, `Platform.Version` is the iOS version, not either value.
code
bash · 5 linescd ios
# Set the build number (CFBundleVersion via CURRENT_PROJECT_VERSION)
xcrun agvtool new-version -all 114
# Set the marketing version (CFBundleShortVersionString via MARKETING_VERSION)
xcrun agvtool new-marketing-version 2.3.1go deeper
Know that the short version is what users see and the build number identifies each upload, and that a re-upload needs a new build number even if the version stays the same.
Explain how Info.plist reads MARKETING_VERSION and CURRENT_PROJECT_VERSION, why both Debug and Release carry them, and how a duplicate build number gets rejected.
Automate the build number from a single increasing source, keep configurations in step, and make sure hotfix uploads never collide with earlier builds.
Treat versioning as a cross-platform contract: one user-facing version per release, separate per-platform build counters, and a clear owner for both.
## Two keys, two audiences Every iOS app declares two version values in its `Info.plist`. A React Native project does not hard-code them there; the template maps them to Xcode build settings: ```xml <key>CFBundleShortVersionString</key> <string>$(MARKETING_VERSION)</string> <key>CFBundleVersion</key> <string>$(CURRENT_PROJECT_VERSION)</string> ``` | | `CFBundleShortVersionString` | `CFBundleVersion` | |---|---|---| | Common name | Version, marketing version | Build, build number | | Build setting | `MARKETING_VERSION` | `CURRENT_PROJECT_VERSION` | | Fresh template value | `1.0` | `1` | | Seen by users | Yes, in the App Store and Settings | No | | Changes on every upload | No | Yes | In Xcode they appear as **Version** and **Build** on the target's General tab, and editing them there writes the build settings. ## What the short version means The **short version** is the release users talk about: `1.4.0` in the store listing, in release notes and in support tickets. It usually follows semantic versioning and changes when you publish a new release to users, not when you rebuild. ## What the build number means The **build number** identifies one binary within a version. The rules that matter in practice: - **An upload needs a build number not yet used for that version.** App Store Connect rejects a duplicate, including for a build that was never submitted for review. - **Keep it increasing.** Teams increment it on every upload and never reuse or decrease it, which also keeps uploads sortable. - **Several builds can share one short version.** A 1.4.0 release might go through builds 57, 58 and 59 before one is submitted. ## The scenario: a parking app's hotfix A parking app has 2.3.0 live. A payment bug needs a fix: 1. The team bumps the short version to 2.3.1 and the build number from 112 to 113, then archives and uploads. 2. The upload is processed but the fix is incomplete, so they patch and re-archive. 3. They keep 2.3.1 but must move the build number to 114; re-uploading 113 would be rejected. 4. Only 2.3.1 (114) is submitted, and users only ever see "2.3.1". The rule of thumb: **the build number counts uploads, the short version counts releases**. ## Managing the numbers in a React Native project - **Set them as build settings**, not by editing `Info.plist` literals, so `$(MARKETING_VERSION)` and `$(CURRENT_PROJECT_VERSION)` stay the single source. - **Automate the build number.** The template uses the `apple-generic` versioning system, so Apple's `agvtool` can bump it from a script; many teams take it from a pipeline counter instead. (Wiring that into CI is covered separately.) - **Keep Release and Debug in step.** Both configurations carry their own `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`; bumping only one is a classic source of confusion. - **Align the short version with Android's user-facing version** if support staff need one number per release. ## Upload errors this causes Most version mistakes surface at upload time rather than at build time: - **Duplicate build number.** The archive was made with a `CURRENT_PROJECT_VERSION` already uploaded for that version; raise it and archive again. - **Bumped in the wrong configuration.** The value was changed for Debug, but **Product > Archive** builds Release, which still carries the old number. - **Version lower than the live one.** A short version that goes backwards is confusing at best; keep it moving forward with each release. - **Hand-edited plist.** Someone replaced `$(CURRENT_PROJECT_VERSION)` with a literal, so later bumps in the General tab silently stop reaching the app. ## Reading the version from JavaScript `Platform.Version` in React Native returns the **OS version** (a string on iOS), not the app's short version or build number. Showing the app version in an About screen needs a native module that reads the bundle's `Info.plist`, typically from a community or Expo library. ## Why interviewers ask this The question separates people who have shipped from people who have only run the Simulator. Someone who has uploaded a rejected duplicate knows the build number is the one that must move, and knows the short version is a promise to users rather than a technical counter.
- Why do React Native templates put $(MARKETING_VERSION) and $(CURRENT_PROJECT_VERSION) in Info.plist instead of literal numbers?So the values live in one place, the target's build settings, which Xcode's General tab and tools such as `agvtool` edit. The plist is filled in at build time. Literal values in the plist would be a second source of truth that drifts from what the General tab shows.
- A React Native iOS upload is rejected for a duplicate build number although the short version was bumped; why?Bumping the short version alone does not change `CFBundleVersion`. Check that `CURRENT_PROJECT_VERSION` was raised in the configuration used to archive, which is Release; a common slip is bumping it only in Debug. Raise the build number above every earlier upload and archive again.
saying these in an interview costs you the question
- Bumping CFBundleShortVersionString alone is enough to re-upload a build.
- CFBundleVersion is the version users see in the App Store.
- A build number can be reused if that build was never submitted for review.
- Platform.Version returns the app's CFBundleShortVersionString.
- Editing Info.plist literals is the normal way to bump versions in the template.