NSUbiquitousKeyValueStore on macOS: iCloud Settings and Reconciliation
Synchronize small app preferences with NSUbiquitousKeyValueStore using change notifications, account-aware reconciliation, and bounded property-list values.
NSUbiquitousKeyValueStore synchronizes a small dictionary of values among devices using the same Apple account. It is appropriate for lightweight preferences such as a selected display mode or a small set of feature options, not for documents, large histories, transactional records, or binary assets. Writes enter a local in-memory store first and are written to disk asynchronously; cloud propagation is also asynchronous and can be unavailable while the device has no active account.
Before designing around the API, check its distribution prerequisites: Apple’s current documentation says the app must be distributed through the App Store or Mac App Store and must request the iCloud key-value store entitlement. An app that lacks the entitlement should treat synchronization as unavailable and preserve a usable local preference path. Validate the signed product’s entitlement in release testing rather than relying only on successful compilation in Xcode.
The API is a synchronization convenience, not a database transaction protocol. A setting can be local before it reaches iCloud, another device can have stale state, and notifications can arrive after the user has changed local preferences again. Applications must reconcile notifications with local UI and define behavior for account changes, quota errors, and conflicting edits.
Keep the synchronized schema small
Store simple property-list-compatible values with stable keys. Apple recommends preferring simple types over custom objects. Use a namespaced key scheme and version any structured dictionary you must keep across releases. Do not serialize an entire application model into one value. Large payloads are harder to reconcile and exceed the intended role of the service.
import Foundation
final class CloudPreferences {
private let store = NSUbiquitousKeyValueStore.default
private var observer: NSObjectProtocol?
func start() {
observer = NotificationCenter.default.addObserver(
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
object: store,
queue: .main
) { [weak self] notification in
self?.reconcile(notification)
}
_ = store.synchronize()
}
func setCompactMode(_ enabled: Bool) {
store.set(enabled, forKey: "com.example.reader.compactMode")
}
private func reconcile(_ notification: Notification) {
let enabled = store.bool(forKey: "com.example.reader.compactMode")
// Update the app model from the current store value, not an old snapshot.
_ = (notification, enabled)
}
deinit {
if let observer { NotificationCenter.default.removeObserver(observer) }
}
}
The example registers for external-change notifications using NotificationCenter.default, as required by the API, and observes only the default ubiquitous store. A real implementation should start observation early, retain the observer token, apply current values to a single settings model, and avoid writing the same received value back in a notification loop.
Treat notifications as reconciliation triggers
The external-change notification can describe server changes, the initial synchronization attempt, a quota violation, or an account change. Its user-info dictionary can include the reason and changed keys. Do not treat every notification as “one preference changed and is ready to overwrite the UI.” Read the reason and changed-key list, then reconcile the affected settings from the current store snapshot.
The notification is a trigger, not a durable event log. If the app was not running when a change arrived, there may be no callback to replay. On activation, re-read the small set of relevant values and merge them with app-local state according to the product’s conflict policy. Never depend on receiving every intermediate value change to reconstruct a sequence of user actions.
When an account-change reason arrives, re-evaluate which cloud values are relevant to the current identity. Don’t assume a value synchronized under the previous Apple account should be applied to the new account. If the product maintains a local account-specific profile, keep its identity separate from this app-scoped key-value store and clear or isolate state at sign-out according to the product’s policy.
Define conflict behavior in the app
If two devices edit different independent preference keys, applying changed keys may be straightforward. If both devices edit the same key while disconnected, the app needs a product rule for what value wins. Do not infer a user-intent conflict policy from the fact that the framework reconciles server and local values. For a simple “last selected view” setting, a last observed value may be acceptable. For a complex multi-field configuration, include a schema and timestamp or use a more appropriate synchronization model with explicit merge semantics.
Avoid read-modify-write operations over a shared key as if they were atomic across devices. For example, a shared counter increment can be lost if two devices read the same prior value and later write competing values. If correctness depends on a globally consistent counter or a sequence of transactions, use a server-backed or database-backed design rather than key-value synchronization.
Keep the app’s UI changes local and responsive. A user’s toggle should update the local model immediately, then persist the value into the ubiquitous store. If cloud synchronization later fails, decide whether the local preference remains valid and how to surface its unsynchronized state. Do not roll back a local control merely because the network is offline unless the setting’s product meaning requires central authority.
Quota and payload discipline
The store has size constraints and reports quota violation changes. Use a bounded set of keys and small values, and remove obsolete keys when migrating. Do not use the service for event logs, image blobs, document contents, or unbounded arrays. Keep a local schema inventory so you can explain what is synchronized and estimate total footprint.
When a quota notification occurs, record the affected key names and app version without logging full preference values. Identify which writes caused growth and preserve local settings while the issue is diagnosed. Repeatedly retrying a value that exceeds the quota will not fix the underlying payload problem.
Migration and key lifecycle
When renaming a key, read the new key and, if absent, migrate from the old key only after validating its type and allowed values. Write the new key, then remove the old one when backward compatibility policy permits. Devices running older app versions may still write the legacy key; keep a compatibility horizon or explicitly define how mixed versions are handled.
Treat enum raw strings and versioned dictionaries as external input. A newer app may write a choice unknown to an older app that remains installed on another device. Older code should fall back safely without overwriting the unknown value with a default unless that behavior is intentional. Preserve unrecognized values when possible so downgrade and forward compatibility do not destroy information.
Keep local defaults and ubiquitous defaults distinct. UserDefaults registration-domain values are local fallback values, while NSUbiquitousKeyValueStore is a cloud-shared preference source. Decide precedence explicitly: a local managed value might override a cloud preference, or a cloud value might initialize a local value once. Do not implement precedence by arbitrarily copying values on every launch.
Keep cloud preferences out of critical workflows
Do not use this store for purchase entitlements, authentication state, document save completion, or security-sensitive preferences. Apple’s current NSUbiquitousKeyValueStore documentation explicitly says its information is stored on disk in an unencrypted format. Use Keychain for personal or sensitive information and an authoritative service for access control.
Do not make app startup wait indefinitely for iCloud. Load local state, register observers, and reconcile when the system supplies current values. If the product cannot function without a synchronized setting, make that dependency and offline behavior explicit rather than blocking an entire UI on an unavailable account.
Testing and observability
Test two devices on the same account, one device offline, initial sync, same-key edits on both devices, account sign-out and sign-in, quota violation, app launch after a remote update, unknown future enum value, and a migration from old keys. Assert the visible value, persisted local choice, cloud value, and change reason separately. A successful local setter does not prove that the value reached another device.
Record the reason code, changed key names, migration version, and local-versus-cloud reconciliation result. Avoid telemetry containing the preference values themselves when they reveal user behavior. Include a support diagnostic that reports whether iCloud settings are enabled and whether the app observed an initial or server change without exposing account identifiers.
NSUbiquitousKeyValueStore is useful for small cross-device settings when the app tolerates eventual updates. It does not make a multi-key edit atomic or define a conflict policy for your product. Keep values bounded, observe changes early, reconcile from current state, and choose a stronger storage model when correctness requires transactions.
Related:
- UserDefaults on macOS: Preference Keys, Registration, and Migration
- CloudKit Zone Change Sync: Durable Tokens, Tombstones, and Local Transactions
Sources: