基于iCloud同步UserDefaults的最佳实践及冲突解决
UserDefaults与iCloud同步的旧数据覆盖问题
我开发的应用将各类设置与状态存储在UserDefaults中,希望实现多设备间的自动同步。但目前尝试的所有方案都会出现最新设置丢失的问题:比如用户离线一段时间后,或装有旧版UserDefaults数据的设备启动并上传旧数据,导致持有新数据的设备下载过时信息。
我已尝试使用用于同步UserDefaults与iCloud的Swift包Zephyr、GitHub上的同类Swift文件DefaultsCloudSync,还基于DefaultsCloudSync编写了自定义类,但均出现相同问题。
希望了解其他应用的解决方式,或是否存在完美解决方案,欢迎提供建议与见解。
感谢您的帮助!
以下是我的自定义iCloud服务类(代码有待优化):
class iCloudBackupService: NSObject { class func manualUpload() -> Result<Void, Error> { do { let defaultsDictionary = UserDefaults.standard.dictionaryRepresentation() let cloudStore = NSUbiquitousKeyValueStore.default for (key, obj) in defaultsDictionary { cloudStore.set(obj, forKey: key as String) } // 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) } cloudStore.synchronize() let excludedUserDefaults = UserDefaults() excludedUserDefaults.set(Date(), forKey: "cloudBackupLastUpload") print("Manually Updated iCloud") NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil); UserDefaults.standard.set(Date(), forKey: "icloudLastBackupDate") UserDefaults.standard.set("manual", forKey: "icloudLastBackupTrigger") NotificationCenter.default.addObserver(self, selector: #selector(self.updateiCloudFromUserDefaults(notification:)), name: UserDefaults.didChangeNotification, object: nil) return .success(()) } catch let error { return .failure(error) } } class func manualRestore() -> Result<Void, Error> { do { // Your code to execute if localDate is older than or equal to cloudDate. stop() let iCloudDictionary = NSUbiquitousKeyValueStore.default.dictionaryRepresentation let userDefaults = UserDefaults.standard for (key, obj) in iCloudDictionary { userDefaults.set(obj, forKey: key as String) } userDefaults.synchronize() let excludedUserDefaults = UserDefaults() excludedUserDefaults.set(Date(), forKey: "cloudBackupLastLocalUpdate") print("Manually Updated local UserDefaults") if userDefaults.bool(forKey: "iCloudBackup") == true { start() } return .success(()) } catch let error { return .failure(error) } } class func start() { // 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?) { // Prevent loop of notifications by removing our observer before updating UserDefaults NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil) 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)") // Re-enable UserDefaultsDidChangeNotification notifications NotificationCenter.default.addObserver(self, selector: #selector(self.updateUserDefaultsFromiCloud(notification:)), name: UserDefaults.didChangeNotification, object: nil) } @objc class func updateiCloudFromUserDefaults(notification: NSNotification?) { // Remove the observer before adding it again NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil) 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 } // 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)") // Add the observer again 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); } }
解决方案建议
1. 为每个设置项添加时间戳
核心是避免盲目的全量覆盖,给每个UserDefaults键值对附加最后修改时间戳:
- 本地更新设置时,同时更新对应键的
[key]_timestamp值为当前时间 - 从iCloud拉取数据时,对每个键对比本地与云端时间戳,仅保留时间戳更新的版本
- 上传本地数据到iCloud时,先对比云端时间戳,只有本地更新时才上传
2. 区分首次同步与增量同步
- 首次启动应用时,优先拉取云端数据(若存在),避免旧本地数据覆盖云端
- 日常同步采用增量方式,仅同步有变化的键,而非全量替换整个UserDefaults
3. 修复当前全量覆盖的逻辑问题
你的自定义类中updateUserDefaultsFromiCloud方法直接用云端所有数据覆盖本地,这是旧数据覆盖新数据的主要原因。修改方向:
- 遍历云端每个键,先获取本地对应的时间戳,仅当云端时间戳更新时才覆盖本地值
- 避免直接删除云端不存在的本地键,除非是用户主动删除的设置
4. 利用NSUbiquitousKeyValueStore的变更通知细节
处理NSUbiquitousKeyValueStore.didChangeExternallyNotification时,通过通知userInfo获取变更的具体键,只同步发生变化的键:
if let changedKeys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] { for key in changedKeys { // 对比时间戳后决定是否更新本地 } }
5. 增加冲突处理机制
对于无法通过时间戳判断的冲突(如两台设备同时修改同一设置):
- 保留双方版本,让用户手动选择
- 针对不同设置类型制定规则(比如数值类型取最大值,文本类型保留最新修改的)
内容的提问来源于stack exchange,提问作者Victor Lobe
相关产品推荐
相关产品推荐

