Как получить версию и номер сборки приложения в Swift: полное объяснение с кодом
Вы когда-нибудь задумывались, почему в App Store одно и то же приложение может называться «МойМессенджер 3.2», а в настройках телефона — «3.2.1 (45)»? Или почему разработчики просят прислать «номер сборки», когда вы сообщаете о баге? Ответ прост: внутри каждого iOS-приложения живут два скрытых числа — версия приложения и номер сборки. И сегодня мы научимся извлекать их с помощью Swift. Это один из самых популярных вопросов на Stack Overflow (десятки тысяч просмотров!), и не зря: знание этих двух строк кода может спасти вам часы отладки и нервов пользователей.
В этой статье мы не просто скопируем ответ с форума. Мы разберём, что такое версия и билд, зачем они нужны, как их правильно получить и, самое главное, как не наступить на грабли. Поехали!
Что такое версия (version) и номер сборки (build number)?
Прежде чем писать код, давайте разберёмся с терминами. В Xcode, в файле Info.plist (или в настройках проекта), есть два ключа:
- CFBundleShortVersionString — это то, что пользователь видит в App Store и в настройках приложения. Обычно выглядит как «1.0», «2.3.1» и т.д. Это версия для пользователя.
- CFBundleVersion — это внутренний номер сборки. Он может быть любым: «42», «20240315», «1.0.0.123». Это версия для разработчика, которая помогает отличать разные сборки одной и той же версии.
Зачем разделять? Представьте, что вы выпустили версию 1.0, но нашли критический баг. Вы исправляете его, не меняя функционала — это всё ещё версия 1.0 для пользователя, но номер сборки вы увеличиваете (например, с 1 на 2). Так вы и пользователя не путаете, и в Crashlytics видите, какая именно сборка упала.
Самый простой способ: Bundle.main
Apple предоставляет удобный доступ к этим данным через объект Bundle.main. Вот классическое решение, которое вы найдёте на Stack Overflow:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Получаем версию приложения
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
// Получаем номер сборки
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
print("Версия приложения: \(appVersion)")
print("Номер сборки: \(buildNumber)")
// Пример использования в UI
versionLabel.text = "Версия \(appVersion) (сборка \(buildNumber))"
}
}
Всё гениальное просто. Но давайте разберём каждую строчку:
Bundle.main— это главный бандл вашего приложения (файл .app).infoDictionary— словарь, который читаетInfo.plist.- Мы используем опциональное приведение через
as? Stringи оператор??на случай, если ключа вдруг нет (хотя это маловероятно).
Продвинутый подход: читаем из ProcessInfo
Хотите выглядеть как профи? Используйте ProcessInfo. Этот класс знает о системе больше, чем просто plist. Но для версии приложения он не даёт ничего нового, зато может показать операционную систему. Однако есть один трюк: если вы оборачиваете получение данных в extension, код становится чище и переиспользуемее.
import Foundation
extension Bundle {
var appVersion: String {
return infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
}
var buildNumber: String {
return infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
}
var fullVersion: String {
return "Версия \(appVersion) (сборка \(buildNumber))"
}
}
// Использование
print(Bundle.main.fullVersion) // "Версия 1.0 (сборка 42)"
Этот подход особенно полезен, если вы выводите версию в нескольких местах (например, в настройках, в алерте о баге, в логах). Не нужно каждый раз писать одну и ту же цепочку — просто вызывайте Bundle.main.fullVersion.
А что если я использую SwiftUI?
Для SwiftUI код практически не меняется. Вы всё так же обращаетесь к Bundle.main. Единственное отличие — вы можете использовать @State или вычисляемое свойство для реактивности:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Приложение")
.font(.title)
Text("Версия: \(Bundle.main.appVersion)")
Text("Сборка: \(Bundle.main.buildNumber)")
}
.padding()
}
}
// Не забудьте про extension, который мы написали выше!
Обратите внимание: Bundle.main не является ObservableObject, поэтому если вы динамически меняете версию (что вряд ли), нужно будет обновлять экран вручную. Но обычно версия зашита в билд и не меняется во время работы приложения.
Почему это может не сработать? Типичные ошибки
На Stack Overflow под этим вопросом — сотни комментариев от разработчиков, у которых код не работал. Вот три главные причины:
- Вы обращаетесь к неправильному бандлу. Если вы пишете библиотеку или расширение (Siri Intent, Widget), у вас может быть свой бандл. В этом случае используйте
Bundle(for: type(of: self))или передайте нужный бандл явно. - Ключи в Info.plist переименованы. Иногда разработчики (или автоматические скрипты) меняют ключи. Проверьте, что у вас есть
CFBundleShortVersionStringиCFBundleVersionв вашемInfo.plist. - Вы используете симулятор с тестовым билдом. На симуляторе всё работает, но на реальном устройстве — нет? Проверьте схему сборки (Scheme). Иногда Xcode генерирует временный Info.plist без этих ключей.
Чтобы отловить такие ошибки, добавьте проверку:
guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
fatalError("Info.plist does not contain CFBundleShortVersionString!")
}
Но в продакшене лучше не использовать fatalError — просто покажите "Unknown".
Зачем это нужно в реальном проекте?
Теперь, когда вы умеете получать версию и билд, давайте подумаем, где это применять. Вот три killer-фичи:
- Экран «О приложении» — пользователи любят видеть версию. Это создаёт доверие и помогает в поддержке.
- Отправка в аналитику — при каждом запуске отправляйте версию и билд в Firebase или Amplitude. Тогда вы точно будете знать, в какой сборке произ