Problems with NSUbiquitousKeyValueStore

83 Views Asked by At

I have tried to implement NSUbiquitousKeyValueStore into my app. So far, it works with a custom class I made, but there are a few problems with it:

1 When I open the app on a device that has an "old database" (e.g., the app was launched a few days ago and another device has newer data), the data gets overwritten with the old one. I've tried to implement a separate Date variable that keeps track of the backup timestamp, but that didn't work. Sometimes, this even happens on the same device. If Automatic Backup is enabled in my app (Upload/Download on UserDefaults changes), the data gets overwritten with old data from the same device.

2 When I create a manual backup and restore it within 4-5 minutes, the new backup doesn't get loaded; only the old one from before. I don't know if this is an Apple server "problem," but maybe there is a workaround (for the other devices too).

3 I have the same app on my M1 Mac (as a native iPad app). The Backup service worked before, but for the past month, I can't restore new backups from my other devices. When I hit restore, the old database from the Mac app gets downloaded. It's as if this app has different Bundle Identifiers (which it obviously doesn't have).

A few points to notice:

-The majority of the UserDefaults data are encoded structs. -The app should have an automatic Backup Mode and a Manual one. -In the best case, the user can select from multiple old backups (like iCloud Backup on iOS). -The issues are present on iOS 15-17 (all supported versions) on both beta and release versions. -I have tried multiple Swift Packages, implemented the solution according to the Apple Documentation, and crafted many solutions myself.

This is my custom iCloud class:

import UIKit

enum iCloudSyncErrorCode: Error {
    case manualRestoreFailed
    case manualBackupFailedWrite
    case manualBackupFailedRemove
    case userIsNotLoggedIntoiCloud
}

class iCloudBackupService: NSObject {

class func manualUpload() -> Result<Void, Error> {
    guard userIsLoggedIntoiCloud() == true else {
        return .failure(iCloudSyncErrorCode.userIsNotLoggedIntoiCloud)
    }
    do {
        // Clean up and update notifications
        let observer = self
        NotificationCenter.default.removeObserver(observer, name: UserDefaults.didChangeNotification, object: nil)
        
        let defaultsDictionary = UserDefaults.standard.dictionaryRepresentation()
        let cloudStore = NSUbiquitousKeyValueStore.default
        
        let allKeys = NSUbiquitousKeyValueStore.default.dictionaryRepresentation.keys
        for key in allKeys {
            NSUbiquitousKeyValueStore.default.removeObject(forKey: key)
        }
        // Save local data to iCloud
        for (key, obj) in defaultsDictionary {
            cloudStore.set(obj, forKey: key)
        }
        
        // Synchronize the cloud store
        if !cloudStore.synchronize() {
            // Handle synchronization failure
            return .failure(iCloudSyncErrorCode.manualBackupFailedWrite)
        }
        
        // Get the keys that need to be removed
        let keysToRemoveSpecific = cloudStore.dictionaryRepresentation.keys.filter { key in
            !defaultsDictionary.keys.contains(key)
        }
        
        // Remove the keys from the dictionary
        for key in keysToRemoveSpecific {
            cloudStore.removeObject(forKey: key)
        }
        
        // Set a timestamp for the update
        let timestampKey = "cloudValDate"
        cloudStore.set(Date(), forKey: timestampKey)
        
        // Synchronize again after removing objects
        if !cloudStore.synchronize() {
            return .failure(iCloudSyncErrorCode.manualBackupFailedRemove)
        }
        
        // Update user defaults for manual backup
        let excludedUserDefaults = UserDefaults()
        excludedUserDefaults.set(Date(), forKey: "cloudBackupLastUpload")
        
        // Print success message
        print("Manually Updated iCloud")
        
        
        UserDefaults.standard.set(Date(), forKey: "icloudLastBackupDate")
        UserDefaults.standard.set("manual", forKey: "icloudLastBackupTrigger")
        
        cloudStore.synchronize()
        
        if UserDefaults.standard.bool(forKey: "iCloudBackup") {
            NotificationCenter.default.addObserver(observer, selector: #selector(updateiCloudFromUserDefaults(notification:)), name: UserDefaults.didChangeNotification, object: nil)
        }
        return .success(())
    } catch let error {
        // Handle any other errors that might occur
        return .failure(error)
    }
}

class func manualRestore() -> Result<Void, Error> {
    guard userIsLoggedIntoiCloud() == true else {
        return .failure(iCloudSyncErrorCode.userIsNotLoggedIntoiCloud)
    }
    do {
        var tempiCloudUD = false // Initialize tempiCloudUD as needed
        
        // Your code to execute if localDate is older than or equal to cloudDate.
        tempiCloudUD = UserDefaults.standard.bool(forKey: "iCloudBackup")
        stop()
        
        let iCloudDictionary = NSUbiquitousKeyValueStore.default.dictionaryRepresentation
        let userDefaults = UserDefaults.standard
        
        // 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)
        }
        
        // Update local backup date and trigger
        UserDefaults.standard.set(Date(), forKey: "icloudLastBackupDate")
        UserDefaults.standard.set("manual", forKey: "icloudLastBackupTrigger")
        print("Manually Updated local UserDefaults")
        
        // Restore tempiCloudUD and start if needed
        UserDefaults.standard.set(tempiCloudUD, forKey: "iCloudBackup")
        if userDefaults.bool(forKey: "iCloudBackup") == true {
            start()
        }
        
        return .success(())
    } catch let error {
        return .failure(error)
    }
}

class func start() {
    guard userIsLoggedIntoiCloud() == true else {
        return
    }
    // Note: NSUbiquitousKeyValueStoreDidChangeExternallyNotification is sent only upon a change received from iCloud, not when your app (i.e., the same instance) sets a value.
    NotificationCenter.default.addObserver(self, selector: #selector(self.updateUserDefaultsFromiCloud(notification:)), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.updateiCloudFromUserDefaults(notification:)), name: UserDefaults.didChangeNotification, object: nil)
    
    print("CloudBackup service started")
    print("Enabled automatic synchronization of NSUserDefaults and iCloud.")
}

@objc class func updateUserDefaultsFromiCloud(notification: NSNotification?) {
    guard userIsLoggedIntoiCloud() == true else {
        return
    }
    
    
    guard UserDefaults.standard.bool(forKey: "iCloudBackup") == true else {
        stop()
        return
    }
    
    // Prevent loop of notifications by removing our observer before updating UserDefaults
    NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil);
    NotificationCenter.default.removeObserver(self, name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil);
    
    tempiCloudUD = UserDefaults.standard.bool(forKey: "iCloudBackup")
    let iCloudDictionary = NSUbiquitousKeyValueStore.default.dictionaryRepresentation
    let userDefaults = UserDefaults.standard
    
    // Update UserDefaults with values from iCloud
    for (key, obj) in iCloudDictionary {
        userDefaults.set(obj, forKey: key as String)
    }
    userDefaults.synchronize()
    
    // Increment iCloud session automatic download times
    iCloudSessionAutomaticDownloadTimes += 1
    
    print("iCloudBackupService: Automatic Download. Times this Session: \(iCloudSessionAutomaticDownloadTimes)")
    UserDefaults.standard.set(Date(), forKey: "icloudLastBackupDate")
    UserDefaults.standard.set("automatic", forKey: "icloudLastBackupTrigger")
    UserDefaults.standard.set(tempiCloudUD, forKey: "iCloudBackup")
    
    // Re-enable UserDefaultsDidChangeNotification notifications
    NotificationCenter.default.addObserver(self, selector: #selector(self.updateUserDefaultsFromiCloud(notification:)), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.updateiCloudFromUserDefaults(notification:)), name: UserDefaults.didChangeNotification, object: nil)
}


@objc class func updateiCloudFromUserDefaults(notification: NSNotification?) { //Upload
    // Remove the observer before adding it again
    guard userIsLoggedIntoiCloud() == true else {
        return
    }
    
    guard UserDefaults.standard.bool(forKey: "iCloudBackup") == true else {
        stop()
        return
    }
    
    NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil);
    NotificationCenter.default.removeObserver(self, name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil);
    UserDefaults.standard.set(Date(), forKey: "iCloudServiceUploadTimestamp")
    
    let defaultsDictionary = UserDefaults.standard.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 need to be removed
    let keysToRemove = cloudStore.dictionaryRepresentation.keys.filter { key in
        !defaultsDictionary.keys.contains(key)
    }
    
    // Remove the keys from the dictionary
    for key in keysToRemove {
        cloudStore.removeObject(forKey: key)
        printKey = key
        keyIsRemovable = true
    }
    
    // let iCloud know that new or updated keys, values are ready to be uploaded
    cloudStore.synchronize()
    
    // Increment iCloud session automatic upload times
    iCloudSessionAutomaticUploadTimes += 1
    
    print("iCloudBackupService: Automatic Upload. Times this Session: \(iCloudSessionAutomaticUploadTimes) Current Object: \(printKey). KeyIsRemovable: \(keyIsRemovable)" )
    UserDefaults.standard.set(Date(), forKey: "icloudLastBackupDate")
    UserDefaults.standard.set("automatic", forKey: "icloudLastBackupTrigger")
    
    // Add the observer again
    NotificationCenter.default.addObserver(self, selector: #selector(self.updateUserDefaultsFromiCloud(notification:)), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.updateiCloudFromUserDefaults(notification:)), name: UserDefaults.didChangeNotification, object: nil)
}

class func stop() {
    NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil);
    NotificationCenter.default.removeObserver(self, name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil);
    print("CloudBackup service stopped")
}

deinit {
    NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil);
    NotificationCenter.default.removeObserver(self, name: 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" gets started by simply calling iCloudBackupService.start().

Thanks in advance!

0

There are 0 best solutions below