ios – Issues with NSUbiquitousKeyValueStore


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!

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles