I’ve tried to implement NSUbiquitousKeyValueStore into my app. To this point, it really works with a customized class I made, however there are just a few issues with it:
1
Once I open the app on a tool that has an “outdated database” (e.g., the app was launched just a few days in the past and one other gadget has newer information), the info will get overwritten with the outdated one. I’ve tried to implement a separate Date variable that retains monitor of the backup timestamp, however that did not work. Generally, this even occurs on the identical gadget. If Automated Backup is enabled in my app (Add/Obtain on UserDefaults adjustments), the info will get overwritten with outdated information from the identical gadget.
2
Once I create a handbook backup and restore it inside 4-5 minutes, the brand new backup would not get loaded; solely the outdated one from earlier than. I do not know if that is an Apple server “drawback,” however perhaps there’s a workaround (for the opposite units too).
3
I’ve the identical app on my M1 Mac (as a local iPad app). The Backup service labored earlier than, however for the previous month, I can not restore new backups from my different units. Once I hit restore, the outdated database from the Mac app will get downloaded. It is as if this app has totally different Bundle Identifiers (which it clearly would not have).
A number of factors to note:
-The vast majority of the UserDefaults information are encoded structs.
-The app ought to have an automated Backup Mode and a Guide one.
-In the perfect case, the consumer can choose from a number of outdated backups (like iCloud Backup on iOS).
-The problems are current on iOS 15-17 (all supported variations) on each beta and launch variations.
-I’ve tried a number of Swift Packages, carried out the answer in line with the Apple Documentation, and crafted many options myself.
That is my customized iCloud class:
import UIKit
enum iCloudSyncErrorCode: Error {
case manualRestoreFailed
case manualBackupFailedWrite
case manualBackupFailedRemove
case userIsNotLoggedIntoiCloud
}
class iCloudBackupService: NSObject {
class func manualUpload() -> Consequence<Void, Error> {
guard userIsLoggedIntoiCloud() == true else {
return .failure(iCloudSyncErrorCode.userIsNotLoggedIntoiCloud)
}
do {
// Clear up and replace notifications
let observer = self
NotificationCenter.default.removeObserver(observer, identify: UserDefaults.didChangeNotification, object: nil)
let defaultsDictionary = UserDefaults.commonplace.dictionaryRepresentation()
let cloudStore = NSUbiquitousKeyValueStore.default
let allKeys = NSUbiquitousKeyValueStore.default.dictionaryRepresentation.keys
for key in allKeys {
NSUbiquitousKeyValueStore.default.removeObject(forKey: key)
}
// Save native information to iCloud
for (key, obj) in defaultsDictionary {
cloudStore.set(obj, forKey: key)
}
// Synchronize the cloud retailer
if !cloudStore.synchronize() {
// Deal with synchronization failure
return .failure(iCloudSyncErrorCode.manualBackupFailedWrite)
}
// Get the keys that should be eliminated
let keysToRemoveSpecific = cloudStore.dictionaryRepresentation.keys.filter { key in
!defaultsDictionary.keys.accommodates(key)
}
// Take away the keys from the dictionary
for key in keysToRemoveSpecific {
cloudStore.removeObject(forKey: key)
}
// Set a timestamp for the replace
let timestampKey = "cloudValDate"
cloudStore.set(Date(), forKey: timestampKey)
// Synchronize once more after eradicating objects
if !cloudStore.synchronize() {
return .failure(iCloudSyncErrorCode.manualBackupFailedRemove)
}
// Replace consumer defaults for handbook backup
let excludedUserDefaults = UserDefaults()
excludedUserDefaults.set(Date(), forKey: "cloudBackupLastUpload")
// Print success message
print("Manually Up to date iCloud")
UserDefaults.commonplace.set(Date(), forKey: "icloudLastBackupDate")
UserDefaults.commonplace.set("handbook", forKey: "icloudLastBackupTrigger")
cloudStore.synchronize()
if UserDefaults.commonplace.bool(forKey: "iCloudBackup") {
NotificationCenter.default.addObserver(observer, selector: #selector(updateiCloudFromUserDefaults(notification:)), identify: UserDefaults.didChangeNotification, object: nil)
}
return .success(())
} catch let error {
// Deal with another errors which may happen
return .failure(error)
}
}
class func manualRestore() -> Consequence<Void, Error> {
guard userIsLoggedIntoiCloud() == true else {
return .failure(iCloudSyncErrorCode.userIsNotLoggedIntoiCloud)
}
do {
var tempiCloudUD = false // Initialize tempiCloudUD as wanted
// Your code to execute if localDate is older than or equal to cloudDate.
tempiCloudUD = UserDefaults.commonplace.bool(forKey: "iCloudBackup")
cease()
let iCloudDictionary = NSUbiquitousKeyValueStore.default.dictionaryRepresentation
let userDefaults = UserDefaults.commonplace
// Save objects from iCloudDictionary to userDefaults
for (key, obj) in iCloudDictionary {
userDefaults.set(obj, forKey: key as String)
}
// Synchronize userDefaults
if !userDefaults.synchronize() {
return .failure(iCloudSyncErrorCode.manualRestoreFailed)
}
// Replace native backup date and set off
UserDefaults.commonplace.set(Date(), forKey: "icloudLastBackupDate")
UserDefaults.commonplace.set("handbook", forKey: "icloudLastBackupTrigger")
print("Manually Up to date native UserDefaults")
// Restore tempiCloudUD and begin if wanted
UserDefaults.commonplace.set(tempiCloudUD, forKey: "iCloudBackup")
if userDefaults.bool(forKey: "iCloudBackup") == true {
begin()
}
return .success(())
} catch let error {
return .failure(error)
}
}
class func begin() {
guard userIsLoggedIntoiCloud() == true else {
return
}
// Be aware: NSUbiquitousKeyValueStoreDidChangeExternallyNotification is distributed solely upon a change acquired from iCloud, not when your app (i.e., the identical occasion) units a worth.
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUserDefaultsFromiCloud(notification:)), identify: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.updateiCloudFromUserDefaults(notification:)), identify: UserDefaults.didChangeNotification, object: nil)
print("CloudBackup service began")
print("Enabled automated synchronization of NSUserDefaults and iCloud.")
}
@objc class func updateUserDefaultsFromiCloud(notification: NSNotification?) {
guard userIsLoggedIntoiCloud() == true else {
return
}
guard UserDefaults.commonplace.bool(forKey: "iCloudBackup") == true else {
cease()
return
}
// Stop loop of notifications by eradicating our observer earlier than updating UserDefaults
NotificationCenter.default.removeObserver(self, identify: UserDefaults.didChangeNotification, object: nil);
NotificationCenter.default.removeObserver(self, identify: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil);
tempiCloudUD = UserDefaults.commonplace.bool(forKey: "iCloudBackup")
let iCloudDictionary = NSUbiquitousKeyValueStore.default.dictionaryRepresentation
let userDefaults = UserDefaults.commonplace
// Replace UserDefaults with values from iCloud
for (key, obj) in iCloudDictionary {
userDefaults.set(obj, forKey: key as String)
}
userDefaults.synchronize()
// Increment iCloud session automated obtain instances
iCloudSessionAutomaticDownloadTimes += 1
print("iCloudBackupService: Automated Obtain. Occasions this Session: (iCloudSessionAutomaticDownloadTimes)")
UserDefaults.commonplace.set(Date(), forKey: "icloudLastBackupDate")
UserDefaults.commonplace.set("automated", forKey: "icloudLastBackupTrigger")
UserDefaults.commonplace.set(tempiCloudUD, forKey: "iCloudBackup")
// Re-enable UserDefaultsDidChangeNotification notifications
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUserDefaultsFromiCloud(notification:)), identify: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.updateiCloudFromUserDefaults(notification:)), identify: UserDefaults.didChangeNotification, object: nil)
}
@objc class func updateiCloudFromUserDefaults(notification: NSNotification?) { //Add
// Take away the observer earlier than including it once more
guard userIsLoggedIntoiCloud() == true else {
return
}
guard UserDefaults.commonplace.bool(forKey: "iCloudBackup") == true else {
cease()
return
}
NotificationCenter.default.removeObserver(self, identify: UserDefaults.didChangeNotification, object: nil);
NotificationCenter.default.removeObserver(self, identify: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil);
UserDefaults.commonplace.set(Date(), forKey: "iCloudServiceUploadTimestamp")
let defaultsDictionary = UserDefaults.commonplace.dictionaryRepresentation()
let cloudStore = NSUbiquitousKeyValueStore.default
var printKey = ""
var keyIsRemovable = false
for (key, obj) in defaultsDictionary {
cloudStore.set(obj, forKey: key)
printKey = key
keyIsRemovable = false
}
cloudStore.set(Date(), forKey: "cloudValDate")
// Get the keys that should be eliminated
let keysToRemove = cloudStore.dictionaryRepresentation.keys.filter { key in
!defaultsDictionary.keys.accommodates(key)
}
// Take away the keys from the dictionary
for key in keysToRemove {
cloudStore.removeObject(forKey: key)
printKey = key
keyIsRemovable = true
}
// let iCloud know that new or up to date keys, values are able to be uploaded
cloudStore.synchronize()
// Increment iCloud session automated add instances
iCloudSessionAutomaticUploadTimes += 1
print("iCloudBackupService: Automated Add. Occasions this Session: (iCloudSessionAutomaticUploadTimes) Present Object: (printKey). KeyIsRemovable: (keyIsRemovable)" )
UserDefaults.commonplace.set(Date(), forKey: "icloudLastBackupDate")
UserDefaults.commonplace.set("automated", forKey: "icloudLastBackupTrigger")
// Add the observer once more
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUserDefaultsFromiCloud(notification:)), identify: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.updateiCloudFromUserDefaults(notification:)), identify: UserDefaults.didChangeNotification, object: nil)
}
class func cease() {
NotificationCenter.default.removeObserver(self, identify: UserDefaults.didChangeNotification, object: nil);
NotificationCenter.default.removeObserver(self, identify: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil);
print("CloudBackup service stopped")
}
deinit {
NotificationCenter.default.removeObserver(self, identify: UserDefaults.didChangeNotification, object: nil);
NotificationCenter.default.removeObserver(self, identify: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil);
}
func getLastBackupDateOnCloud() -> Date? {
guard userIsLoggedIntoiCloud() == true else {
return Date(timeIntervalSince1970: 0)
}
let iCloudDictionary = NSUbiquitousKeyValueStore.default.dictionaryRepresentation
for (key, obj) in iCloudDictionary {
print(key)
if key == "cloudValDate" {
return obj as? Date
}
}
return nil
}
}
The “Service” will get began by merely calling iCloudBackupService.begin().
Thanks upfront!