Last active
July 17, 2024 08:38
-
-
Save yosshi4486/6ca744cd648e5784df9036f57be44b6e to your computer and use it in GitHub Desktop.
Example of syncing key value stores.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SynchronizingKeyValueStore.swift | |
// | |
// Created by yosshi4486 on 2022/08/30. | |
// | |
import Foundation | |
/// A key value store that stores a key-value into both local and cloud. Subclass this class and override methods for your application. | |
/// | |
/// - SeeAlso: | |
/// [Syncrhonizing App Preferences with iCloud](https://developer.apple.com/documentation/foundation/icloud/synchronizing_app_preferences_with_icloud) | |
/// | |
class SynchronizingKeyValueStore { | |
/// The key-value store for local. | |
var userDefaults: UserDefaults | |
/// The key-value store for cloud. | |
var ubiquitousKeyValueStore: NSUbiquitousKeyValueStore | |
/// The keys of sync key value store. | |
/// | |
/// The default implementation is empty. Override this method for registering your app sync value's keys. | |
var allKeys: Set<String> { [] } | |
/// Creates a new instance by the given `userDefaults` and `ubiquitousKeyValueStore`. | |
/// | |
/// You can do: | |
/// - Inject stubs for testing. | |
/// - Give stores that adopt an App Group feature. | |
/// | |
/// - Parameters: | |
/// - userDefaults: The key-value store that stores values for local. | |
/// - ubiquitousKeyValueStore: The key-value store that stores values for cloud. | |
init(userDefaults: UserDefaults = .standard, ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = .default) { | |
self.userDefaults = userDefaults | |
self.ubiquitousKeyValueStore = ubiquitousKeyValueStore | |
prepareObservingCloudStoreChanges() | |
} | |
/// Registers initial states for local store keys. | |
/// | |
/// The default implementation is empty. Override this method for registering your app states. | |
func registerInitialValues() {} | |
/// Prepares for observing cloud key-value store changes. | |
/// | |
/// Apple officially claim some steps for observing cloud store changes describing bellow: | |
/// 1. Register a `NSUbiquitousKeyValueStore.didChangeExternallyNotification`notification | |
/// 2. Checks whether the `syncronize()` is `true`. | |
/// | |
/// This class automatically calls this method in the `init(userDefaults:ubiquitousKeyValueStore)`. | |
func prepareObservingCloudStoreChanges() { | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(applyRemoteKeyValueStoreChanges(_:)), | |
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, | |
object: ubiquitousKeyValueStore) | |
if ubiquitousKeyValueStore.synchronize() == false { | |
fatalError("Entitlement error. You have to add iCloud capability in Signing&Capabilities tab, then make check to ") | |
} | |
} | |
/// Propagate the local value that is associated with the given key to cloud. | |
/// | |
/// This class automatically calls this method in `applyRemoteKeyValueStoreChanges`, if the value should be unified to local value. | |
func propagateLocalValue(forKey key: String) { | |
let localObject = userDefaults.object(forKey: key) | |
ubiquitousKeyValueStore.set(localObject, forKey: key) | |
} | |
/// Propagate the remote value that is associated with the given key to local. | |
/// | |
/// This class automatically calls this method in `applyRemoteKeyValueStoreChanges`, if the value should be unified to cloud value. | |
func propagateRemoteValue(forKey key: String) { | |
let cloudObject = ubiquitousKeyValueStore.object(forKey: key) | |
userDefaults.set(cloudObject, forKey: key) | |
} | |
/// Applies the remote store changes that include changes to the remote-store itself. ex) Account change, limit exceed, etc.. | |
@objc func applyRemoteKeyValueStoreChanges(_ notification: Notification) { | |
/** Reasons can be: | |
NSUbiquitousKeyValueStoreServerChange: | |
Value(s) were changed externally from other users/devices. | |
Get the changes and update the corresponding keys locally. | |
NSUbiquitousKeyValueStoreInitialSyncChange: | |
Initial downloads happen the first time a device is connected to an iCloud account, | |
and when a user switches their primary iCloud account. | |
Get the changes and update the corresponding keys locally. | |
Note: If you receive "NSUbiquitousKeyValueStoreInitialSyncChange" as the reason, | |
you can decide to "merge" your local values with the server values. | |
NSUbiquitousKeyValueStoreQuotaViolationChange: | |
Your app’s key-value store has exceeded its space quota on the iCloud server of 1mb. | |
NSUbiquitousKeyValueStoreAccountChange: | |
The user has changed the primary iCloud account. | |
The keys and values in the local key-value store have been replaced with those from the new account, | |
regardless of the relative timestamps. | |
> Apple inc., Synchronizing App Preferences with iCloud, PrefsInCloud(Sample Project), ViewController+KVS, ubiquitousKeyValueStoreDidChange, viewed: 2022/08/30 | |
*/ | |
guard | |
let userInfo = notification.userInfo, | |
let reasonForChange = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int, | |
let keys = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] | |
else { | |
return | |
} | |
let intersectedKeys = allKeys.intersection(Set(keys)) | |
guard !intersectedKeys.isEmpty else { return } | |
switch reasonForChange { | |
case NSUbiquitousKeyValueStoreAccountChange: | |
// Fallback to local values | |
intersectedKeys.forEach { propagateLocalValue(forKey: $0) } | |
case NSUbiquitousKeyValueStoreInitialSyncChange: | |
// Apply remote values to local. The defaul merge behavior is "server wins" policy, but you can decide the merge policy in your subclass. | |
intersectedKeys.forEach { propagateRemoteValue(forKey: $0) } | |
case NSUbiquitousKeyValueStoreQuotaViolationChange: | |
fatalError("Your app’s key-value store has exceeded its space quota on the iCloud server of 1mb. This is application state design problem. You have to re-design the app's state architecture.") | |
default: | |
// Apply remote values into local store. | |
intersectedKeys.forEach { propagateRemoteValue(forKey: $0) } | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
An example of subclassing.