CFBundleShortVersionString and CFBundleVersion keys from Bundle.main.infoDictionary. Wrap these lookups in a Bundle extension with optional casting (as? String) to safely access the values without force unwrapping.Why this happens
Apple separates application release metadata into two distinct fields inside your app target’s Info.plist file: the marketing version and the internal build number. In the raw property list, these correspond to the keys CFBundleShortVersionString and CFBundleVersion.
The marketing version (CFBundleShortVersionString) represents the user-facing release string visible on the App Store (such as 2.4.1). The build number (CFBundleVersion) represents the machine-readable iteration compiled by Xcode or CI/CD pipelines (such as 108 or 20261109.1). App Store Connect requires the marketing version to follow standard semantic versioning and mandates that every uploaded build number increments sequentially.
At runtime, iOS stores these values in Bundle.main.infoDictionary. Because infoDictionary returns an optional dictionary typed as [String: Any]?, developers often encounter runtime crashes by force-unwrapping the dictionary or force-casting the values. Accessing these strings safely requires defensive unwrapping and handling multi-bundle targets properly when code runs inside frameworks or Swift Packages.
How to fix it
1. Extend Bundle with Safe Computed Properties (Recommended)
The cleanest approach creates computed properties directly on Bundle. This keeps your call sites readable while guaranteeing that missing keys or unexpected types fall back gracefully rather than crashing.
import Foundation
extension Bundle {
/// The user-facing marketing version (e.g., "1.0.4").
var releaseVersionNumber: String? {
return infoDictionary?["CFBundleShortVersionString"] as? String
}
/// The internal build iteration number (e.g., "42").
var buildVersionNumber: String? {
return infoDictionary?["CFBundleVersion"] as? String
}
/// A formatted string combining both values (e.g., "1.0.4 (42)").
var fullAppVersion: String {
guard let version = releaseVersionNumber else {
return "Version Unavailable"
}
if let build = buildVersionNumber {
return "v\(version) (\(build))"
}
return "v\(version)"
}
}
You can now access these properties anywhere across your application:
let version = Bundle.main.releaseVersionNumber ?? "0.0.0"
let build = Bundle.main.buildVersionNumber ?? "0"
let debugHeader = Bundle.main.fullAppVersion
print("Running \(debugHeader)")
2. Display the Version in a SwiftUI Settings Screen
When presenting version details to users on an “About” or “Settings” screen, bind the extension directly to your SwiftUI view hierarchy. Providing a fallback string ensures Xcode Previews and unit tests render cleanly even if the mock environment lacks a fully populated bundle dictionary.
import SwiftUI
struct AboutAppView: View {
private var appVersionDisplay: String {
Bundle.main.fullAppVersion
}
var body: some View {
List {
Section(header: Text("Application Details")) {
HStack {
Text("Version")
Spacer()
Text(appVersionDisplay)
.foregroundColor(.secondary)
}
}
}
}
}
#Preview {
AboutAppView()
}
3. Read Version Information from Frameworks and Swift Packages
If your telemetry, networking, or diagnostic logger lives inside a modularized dynamic framework or a Swift Package, querying Bundle.main returns the metadata of the host application, not the framework. Depending on your objective, you must choose the correct bundle instance.
To identify the host application’s version from inside a framework, continue using Bundle.main:
// Retrieves the version of the main iOS app hosting this framework
let hostAppVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
To identify the framework’s own version, locate the bundle associated with a class inside that module:
public final class AnalyticsEngine {
public static var frameworkVersion: String? {
let frameworkBundle = Bundle(for: AnalyticsEngine.self)
return frameworkBundle.infoDictionary?["CFBundleShortVersionString"] as? String
}
}
For Swift Packages that define resources, Swift synthesizes Bundle.module automatically:
let packageVersion = Bundle.module.infoDictionary?["CFBundleShortVersionString"] as? String
4. Include App Version in Network Headers and Crash Reports
A primary use case for reading the version string is attaching user-agent headers to outgoing HTTP requests or annotating structured logs.
import Foundation
struct APIClient {
static func makeUserAgent() -> String {
let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "App"
let version = Bundle.main.releaseVersionNumber ?? "UnknownVersion"
let build = Bundle.main.buildVersionNumber ?? "UnknownBuild"
let osVersion = ProcessInfo.processInfo.operatingSystemVersionString
return "\(appName)/\(version) (\(build); iOS \(osVersion))"
}
}
How to prevent it
- Never force-unwrap
Bundle.main.infoDictionarywith!. While standard app targets always produce a dictionary, custom test runners, extensions, and command-line targets can returnnil. - Do not confuse
CFBundleShortVersionStringwithCFBundleVersion. Use the short string for public-facing UIs and analytics dashboards, and use the build number for build tracking and bug reproduction. - Avoid hardcoding version strings in Swift source files. Always let Xcode manage the values through the target’s build settings (
MARKETING_VERSIONandCURRENT_PROJECT_VERSION). - Verify that your test host target includes a valid
Info.plistif your unit tests execute assertions against bundle version helpers.
FAQ
What is the difference between CFBundleShortVersionString and CFBundleVersion?
CFBundleShortVersionString corresponds to the “Marketing Version” displayed to users on the App Store. CFBundleVersion represents the internal build number used by App Store Connect and developers to distinguish between consecutive uploads of the same release.
Why does Bundle.main.infoDictionary return nil in unit tests?
When unit tests run without a configured host application, Bundle.main resolves to the test runner bundle rather than your application bundle. Set the “Host Application” dropdown in your test target’s General settings to your app target to restore access to the app’s Info.plist.
Can users modify the app version and build strings at runtime?
No. The Info.plist file resides inside the signed .app bundle directory. iOS enforces code signing across the entire bundle, meaning any modification to files within the bundle invalidates the signature and prevents the app from launching.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
