Implementing Seamless App Version Management in iOS with CloudKit

Islam Moussa
4 min readAug 1, 2024

--

Keeping your app up-to-date is crucial for providing users with the latest features, bug fixes, and security updates. In this blog post, we’ll explore how to implement a robust app version checking mechanism in iOS using CloudKit. We’ll leverage CloudKit to store and retrieve version information, and we’ll handle force and optional updates through user-friendly alerts.

Setting Up CloudKit

First, you’ll need to set up CloudKit in your iOS project. Ensure that you have an Apple Developer account and that CloudKit is enabled for your app.

Creating the CloudKit Record

In the CloudKit Dashboard:

  1. Create a new record type called Version_qa.
  2. Add two fields to this record type: store_version and minimum_version.
  • store_version represents the version currently available on the App Store.
  • minimum_version represents the minimum version of the app that your system supports, which will trigger a force update if the user's version is lower.

Next, create a record within this record type and set appropriate values for store_version and minimum_version.

Defining the App Version Model

We’ll start by defining a simple model to represent our app version information.

struct AppVersion: Codable {
let storeVersion, minimumVersion: String?
var hasForceUpdate: Bool = false
var hasUpdate: Bool = false
}

This model will hold the store version, minimum required version, and flags to indicate whether a force update or an optional update is needed.

Fetching Version Information with CloudKit

Next, we’ll create a protocol and a manager class to fetch version information from CloudKit.

protocol CloudKitProtocol {
func fetchAppVersion(completion: @escaping (Result<AppVersion, Error>) -> Void)
}

final class CloudKitManager: CloudKitProtocol {
private let container: CKContainer
private let database: CKDatabase

init() {
self.container = CKContainer(identifier: "iCloud.com.your bundle id")
self.database = container.publicCloudDatabase
}

func fetchAppVersion(completion: @escaping (Result<AppVersion, Error>) -> Void) {
let recordID = CKRecord.ID(recordName: "Version_qa")
database.fetch(withRecordID: recordID) { record, error in
if let error = error {
completion(.failure(error))
return
}

guard let record = record,
let storeVersion = record["store_version"] as? String,
let minimumVersion = record["minimum_version"] as? String else {
completion(.failure(NSError(domain: "", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Invalid record format"])))
return
}

let appVersion = AppVersion(storeVersion: storeVersion, minimumVersion: minimumVersion)
completion(.success(appVersion))
}
}
}

In this implementation:

  • The CloudKitManager class conforms to the CloudKitProtocol and provides the functionality to fetch the app version from CloudKit.
  • The fetchAppVersion method fetches the version record from the CloudKit public database and parses it into the AppVersion model.

App Version Manager

We’ll create the AppVersionManager class that will use the CloudKitManager to fetch version information and handle the update prompts.

protocol AppVersionProtocol {
func fetchVersion(completion: @escaping (Result<AppVersion, Error>) -> Void)
func checkVersion()
}

final class AppVersionManager: AppVersionProtocol {
private let manager: CloudKitProtocol
private let appStoreURL: URL

init(manager: CloudKitProtocol, appStoreURL: URL) {
self.manager = manager
self.appStoreURL = appStoreURL
}

func fetchVersion(completion: @escaping (Result<AppVersion, Error>) -> Void) {
manager.fetchAppVersion { result in
switch result {
case .success(let version):
let appVersion = Bundle.main.appVersion
let storeVersion = version.storeVersion ?? ""
let minimumVersion = version.minimumVersion ?? ""

let hasForceUpdate = VersionComparator.isUpdateAvailable(appStoreVersion: minimumVersion,
currentVersion: appVersion)
let hasUpdate = VersionComparator.isUpdateAvailable(appStoreVersion: storeVersion,
currentVersion: appVersion)

let appVersionInfo = AppVersion(storeVersion: version.storeVersion,
minimumVersion: minimumVersion,
hasForceUpdate: hasForceUpdate,
hasUpdate: hasUpdate)

completion(.success(appVersionInfo))
case .failure(let error):
completion(.failure(error))
}
}
}

func checkVersion() {
fetchVersion { [weak self] result in
switch result {
case .success(let version):
guard version.hasForceUpdate || version.hasUpdate else { return }
DispatchQueue.main.async {
self?.promptUserToUpdate(isForce: version.hasForceUpdate)
}
case .failure(let error):
print("Error fetching version: \(error)")
}
}
}

private func promptUserToUpdate(isForce: Bool = false) {
let alert = createUpdateAlert(isForce: isForce)
presentAlert(alert, isForce: isForce)
}

private func createUpdateAlert(isForce: Bool) -> UIAlertController {
let alert = UIAlertController(title: "New Version Available", message: "Please update to the latest version.", preferredStyle: .alert)

let updateAction = UIAlertAction(title: "Update", style: .default) { _ in
self.handleUpdateAction()
}
alert.addAction(updateAction)

if !isForce {
let cancelAction = UIAlertAction(title: "Cancel", style: .destructive, handler: nil)
alert.addAction(cancelAction)
}

return alert
}

private func handleUpdateAction() {
guard UIApplication.shared.canOpenURL(appStoreURL) else { return }

if #available(iOS 10.0, *) {
UIApplication.shared.open(appStoreURL, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(appStoreURL)
}
}

private func presentAlert(_ alert: UIAlertController, isForce: Bool) {
guard let vc = UIApplication.topViewController(), !(vc is UIAlertController) else { return }

let presentAlertBlock = {
if let vc = UIApplication.topViewController() {
vc.present(alert, animated: true)
}
}

if vc is SplashViewController {
DispatchQueue.main.asyncAfter(deadline: .now() + 4, execute: presentAlertBlock)
} else {
DispatchQueue.main.async(execute: presentAlertBlock)
}
}
}

In this implementation:

  • fetchVersion retrieves the version information from CloudKit and compares it with the current app version.
  • checkVersion checks if an update is needed and prompts the user accordingly.
  • promptUserToUpdate creates and presents an alert to the user, indicating whether an update is mandatory or optional.

Version Comparison Utility

We’ll use a utility class to compare version strings.

final class VersionComparator {
static func isUpdateAvailable(appStoreVersion: String, currentVersion: String) -> Bool {
return appStoreVersion.compare(currentVersion, options: .numeric) == .orderedDescending
}
}

Extension for App Version

We’ll add an extension to Bundle to retrieve the current app version.

extension Bundle {
var appVersion: String {
return infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
}
}

Presenting Alerts

Finally, we’ll add a method to get the top view controller to present alerts.

extension UIApplication {
static func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return topViewController(base: selected)
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}

Conclusion

By integrating CloudKit with your iOS app, you can efficiently manage app version checks and updates. This approach ensures that your users always have access to the latest features and security improvements. The presented code provides a robust solution for handling both mandatory and optional updates, enhancing the overall user experience.

Feel free to customize and extend this implementation to suit your specific needs. Happy coding!

--

--

Islam Moussa

Professional iOS Developer, cross-platform developer and backend developer from Egypt