Как разработчики, когда мы выпускаем новую версию нашего приложения с новыми функциями и исправлениями ошибок, мы хотим, чтобы наши пользователи как можно скорее обновились до последней версии.
Однако у многих пользователей на устройствах не включены автоматические обновления, и если они не открывают сам App Store и не ищут доступные обновления, они могут так и не узнать, что вышла новая версия вашего приложения.
Именно поэтому стоит сделать так, чтобы пользователи могли легко узнать о выходе новой версии вашего приложения прямо из самого приложения. Мы только что реализовали новую функцию в Helm, которая делает именно это, поскольку мы заметили, что многие пользователи сообщали об ошибках в старых версиях приложения:
В этой статье я покажу вам различные подходы к проверке последней доступной версии вашего приложения в App Store, которые вы можете использовать для очень простой реализации функции показа или принудительного обновления в вашем приложении.
Используя iTunes Lookup
У Apple есть специальная конечная точка, которую можно использовать для проверки информации о приложении в App Store по идентификатору: https://itunes.apple.com/lookup?id={id} или по идентификатору бандла: https://itunes.apple.com/lookup?bundleId={bundleId}.
Конечная точка возвращает JSON-объект со списком результатов, соответствующих указанному идентификатору, и каждый из результатов содержит информацию о приложении, включая номер версии и минимальную версию ОС, необходимую для запуска приложения:
import Foundation
struct LookUpResponse: Decodable {
let results: [LookUpResult]
struct LookUpResult: Decodable {
let version: String
let minimumOsVersion: String
let trackViewUrl: URL
}
}
struct LatestAppStoreVersion {
let version: String
let minimumOsVersion: String
let upgradeURL: URL
}
final class LookUpAPI {
private let session: URLSession
private let jsonDecoder: JSONDecoder
init(session: URLSession = .shared, jsonDecoder: JSONDecoder = .init()) {
self.session = session
self.jsonDecoder = jsonDecoder
}
func getLatestAvailableVersion(for appID: String) async throws -> LatestAppStoreVersion? {
let url = URL(string: "https://itunes.apple.com/lookup?appId=\(appID)")!
let request = URLRequest(url: url)
let (data, _) = try await session.data(for: request)
let response = try jsonDecoder.decode(LookUpResponse.self, from: data)
print(response)
return response.results.first.map {
.init(version: $0.version,
minimumOsVersion: $0.minimumOsVersion,
upgradeURL: $0.trackViewUrl)
}
}
}
Хотя этот подход прост в реализации, поскольку запрос не требует аутентификации, у него есть довольно серьезное ограничение: он возвращает только один результат для каждого идентификатора приложения, что означает, что если для вашего приложения доступно несколько платформ (iOS, macOS, watchOS и т.д.), вы получите информацию только для одной из них.
Этот подход хорошо подошел нам, поскольку Helm доступен только на macOS, но он не сработает, если у вас есть приложение, которое доступно на нескольких платформах.
Используя App Store Connect API
Более надежный подход, хотя и более сложный, заключается в использовании App Store Connect API для получения всех доступных версий, готовых к распространению для каждой платформы вашего приложения.
К сожалению, App Store Connect API требует аутентификации всех запросов с помощью JWT-токена, который вы должны сгенерировать, используя ключ API, что значительно увеличивает сложность.
Процесс генерации JWT не так прост, поэтому я бы посоветовал вам использовать библиотеку вроде appstoreconnect-swift-sdk, которая сделает это за вас.
После того как аутентификация будет выполнена, вам останется только сделать запрос к конечной точке https://api.appstoreconnect.apple.com/v1/apps/{id}/appStoreVersions с нужными параметрами и фильтрами и сопоставить полученный ответ с вашей моделью:
import Foundation
import AppStoreConnect_Swift_SDK
struct LatestAppStoreVersion {
let version: String
let minimumOsVersion: String
let upgradeURL: URL
}
final class LatestVersionAPI {
let provider: APIProvider
init?() {
guard let configuration = try? APIConfiguration(
issuerID: "🙈",
privateKeyID: "🙈",
privateKey: "🙈") else {
return nil
}
self.provider = APIProvider(configuration: configuration)
}
func getLatestAvailableVersion(for appID: String, platform: Platform) async throws -> LatestAppStoreVersion? {
let filterPlatform: APIEndpoint.V1.Apps.WithID.AppStoreVersions.GetParameters.FilterPlatform = {
switch platform {
case .ios: return .ios
case .macOs: return .macOs
case .tvOs: return .tvOs
case .visionOs: return .visionOs
}
}()
let versionsRequest = APIEndpoint
.v1
.apps
.id(appID)
.appStoreVersions
.get(parameters: .init(filterAppVersionState: [.readyForDistribution],
filterPlatform: [filterPlatform],
fieldsAppStoreVersions: [.versionString, .platform],
fieldsBuilds: [.minOsVersion],
limit: 1,
include: [.build]))
let versionsResponse = try await provider.request(versionsRequest)
let minimumOsVersion: String? = versionsResponse
.included?
.compactMap { item in
if case let .build(build) = item {
return build.attributes?.minOsVersion
}
return nil
}
.first
guard let versionString = versionsResponse.data.first?.attributes?.versionString,
let minimumOsVersion else {
return nil
}
return LatestAppStoreVersion(version: versionString,
minimumOsVersion: minimumOsVersion,
upgradeURL: URL(string: "https://itunes.apple.com/app/id\(appID)")!)
}
}
// Usage
let api = LatestVersionAPI()
let available = try await api?.getLatestAvailableVersion(
for: "1596487035",
platform: .visionOs
)
Сравнение локальной и удаленной версий
Получив последнюю доступную версию приложения в App Store с помощью одного из двух подходов, вы можете сравнить ее с локальной версией вашего приложения и с версией системы, чтобы определить, нужно ли пользователю предлагать обновить приложение:
extension LatestAppStoreVersion {
var shouldUpdate: Bool {
guard let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
return false
}
let systemVersion = ProcessInfo().operatingSystemVersion
let versionString = "\(systemVersion.majorVersion).\(systemVersion.minorVersion).\(systemVersion.patchVersion)"
let isRemoteVersionHigherThanLocal = currentVersion.compare(self.version, options: .numeric) == .orderedAscending
let isSystemVersionAllowed = versionString.compare(self.minimumOsVersion, options: .numeric) == .orderedDescending
return isRemoteVersionHigherThanLocal && isSystemVersionAllowed
}
}

