Implementing Seamless App Version Management in iOS with CloudKit
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:
- Create a new record type called
Version_qa
. - Add two fields to this record type:
store_version
andminimum_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 theCloudKitProtocol
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 theAppVersion
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!