You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 19:08:05