Keeping App Preferences in Sync with NSUbiquitousKeyValueStore
Ever wished your app’s settings would magically appear on all your devices without any extra effort? If a user switches on Dark Mode or changes a preference on their iPhone, wouldn’t it be great if their iPad picked it up too? Good news: Apple’s NSUbiquitousKeyValueStore is an official iCloud solution to do exactly that. In this read, we’ll explore how to use NSUbiquitousKeyValueStore to sync user preferences (and other small, non-sensitive data) across devices in a SwiftUI app. We’ll also briefly compare it with UserDefaults
and CloudKit
, and touch on security considerations. Let’s dive in!
UserDefaults vs. iCloud Key-Value Store vs. CloudKit — What’s the Difference?
Before jumping into code, let’s clarify where NSUbiquitousKeyValueStore fits in, compared to UserDefaults and CloudKit:
- NSUserDefaults (UserDefaults): The go-to for storing user preferences locally on a device. Simple key-value storage, but local only — it doesn’t automatically sync to other devices. Great for things like app settings on one device. (Think: “store it on this phone.”)
- NSUbiquitousKeyValueStore (iCloud Key-Value Store): Basically UserDefaults on iCloud. It’s a key-value store that syncs across the user’s devices via iCloud. If it’s a small piece of data you’d usually put in UserDefaults (a boolean, a string, a number, etc.), but you want all of the user’s devices to stay in sync, NSUbiquitousKeyValueStore is your friend. Apple’s documentation describes it as an iCloud-backed container of key-value pairs shared among a user’s devices.
- CloudKit: A more powerful iCloud framework for storing large or complex data (structured records, files, shared databases). Use this when you need to sync a lot more than just simple preferences — think user-generated content, databases, or files. It’s more complex and requires setting up a CloudKit container, handling records and queries, etc. In short: if the data is something you’d normally put in a database or file, and not just a simple setting, CloudKit is the way to go.
In summary: UserDefaults for local settings, NSUbiquitousKeyValueStore for small settings that roam with iCloud, CloudKit for heavy-duty cloud data. The entire point of iCloud’s key-value store is to easily share user preferences or simple state across devices with minimal fuss. (And if you’re wondering: yes, under the hood the key-value store is part of Apple’s CloudKit services, but you don’t have to interact with CloudKit directly to use it.)
Note: NSUbiquitousKeyValueStore is intended for small data. Apple limits it to 1 MB total per app (and max 1024 keys) for iCloud key-value storage. That’s plenty for preferences or a bit of app state, but if you need to sync larger blobs of data or thousands of entries, you should be looking at CloudKit or another storage solution.
Getting Started — How Does NSUbiquitousKeyValueStore Work?
Using NSUbiquitousKeyValueStore feels very similar to using UserDefaults. You use string keys to save and retrieve values (which can be primitive types like Bool
, Int
, Double
, String
, Data
, arrays, dictionaries, etc.—any property list types). The difference is that the data is stored in iCloud and automatically propagated to the same app on the user’s other devices.
Before you start coding, make sure to enable iCloud Key-Value Storage in your app’s target capabilities in Xcode. This sets up the app to use iCloud for NSUbiquitousKeyValueStore. If the user is logged into iCloud on the device, changes will sync; if not, NSUbiquitousKeyValueStore will still work locally (but won’t sync until iCloud is available).
A few characteristics of NSUbiquitousKeyValueStore to keep in mind:
- It syncs automatically. You generally don’t need to manually push or fetch updates for each change — the system will handle it. (Under good network conditions, changes usually sync within seconds, but this is eventually consistent syncing, not instant realtime messaging.)
- You can manually synchronize if needed. For example, calling
NSUbiquitousKeyValueStore.default.synchronize()
will explicitly tell the store to pull latest values from iCloud and push any pending changes. It’s wise to call this once when your app launches, to fetch any updates that might have been made on another device while your app wasn’t running. After that, trust the system to do the work. - Keys and values work like UserDefaults: keys are strings, and values are saved in memory then written to disk (and cloud) periodically. You set values with
set(_:forKey:)
and retrieve with methods likebool(forKey:)
,string(forKey:)
, etc. - Data types must be property-list compatible (just as with UserDefaults). If you need to store custom objects, you’d serialize them (e.g. to Data or JSON) before storing.
With that in place, let’s see how to implement it in a SwiftUI app.
Implementing iCloud Sync in SwiftUI
To demonstrate, let’s say we have a user preference (for example, a username or a toggle for some setting) that we want to sync. We’ll walk through a simple example using a SwiftUI view and NSUbiquitousKeyValueStore to keep a text field’s content in sync across devices.
1. Saving Preferences to the Cloud
First, we’ll use a piece of state in SwiftUI and update the cloud store whenever that state changes. For instance, imagine a username that the user can set:
import SwiftUI
struct SettingsView: View {
@State private var username: String = NSUbiquitousKeyValueStore.default.string(forKey: "username") ?? ""
var body: some View {
VStack(spacing: 20) {
TextField("Enter username", text: $username)
.textFieldStyle(.roundedBorder)
.padding()
Text("Hello, \(username.isEmpty ? "User" : username)!")
}
.onChange(of: username) { newValue in
// Save the new value to iCloud key-value store whenever it changes
NSUbiquitousKeyValueStore.default.set(newValue, forKey: "username")
}
.onAppear {
// Ensure we have the latest value from iCloud when view appears
NSUbiquitousKeyValueStore.default.synchronize()
}
}
}
In this code:
- We initialize the
@State
username with whatever value is already stored in the cloud (or an empty string if nothing’s there yet). - The
.onChange
modifier watches for changes tousername
(e.g., when the user types in the TextField) and callsset(_:forKey:)
on the iCloud key-value store. This saves the new username to iCloud. You don’t need to callsynchronize()
every time – the system will queue and sync the change for you soon after. (Callingsynchronize()
here won’t hurt, but it’s not necessary.) - In
.onAppear
, we explicitly callsynchronize()
once to fetch any recent value from iCloud that might not yet be in memory. This helps if, for example, the user last changed their username on another device.
2. Listening for Changes from Other Devices
Saving to the cloud is half the story. We also want this device to respond when another device updates the same key. For example, if the user changes their username on their iPad, we want the iPhone’s UI to update automatically.
NSUbiquitousKeyValueStore posts a notification whenever data is updated from iCloud. We can listen for the NSUbiquitousKeyValueStore.didChangeExternallyNotification
to know when a change comes in. In SwiftUI, we can use an .onReceive
modifier with a NotificationCenter publisher to handle this:
.onReceive(NotificationCenter.default.publisher(
for: NSUbiquitousKeyValueStore.didChangeExternallyNotification)) { notification in
// Pull the updated value from iCloud and update our state
if let newName = NSUbiquitousKeyValueStore.default.string(forKey: "username") {
username = newName
}
}
Putting it all together, our SwiftUI view now both writes to iCloud on changes and reads from iCloud when changes occur elsewhere. The user can update their preference on any device, and all other devices will catch up.
Tip: The notification’s user info can tell you which keys changed (accessible via the NSUbiquitousKeyValueStoreChangedKeysKey
) and why (e.g. initial sync vs. update) – but for most use cases, you can simply reload the values you care about when any external change occurs. In our case, we just fetch the username
key’s value when a notification comes in.
3. (Optional) Using AppStorage-like Convenience
The above approach works and is clear. If you find yourself doing this for many keys, it can get repetitive. Just as SwiftUI has @AppStorage
for UserDefaults, you might consider a helper or a library to streamline iCloud key-value usage. For instance, the open-source CloudStorage property wrapper by Tom Lokhorst provides an @CloudStorage
wrapper that works almost exactly like @AppStorage
, but backed by NSUbiquitousKeyValueStore. Using such a wrapper, our example could become as simple as:
@CloudStorage("username") var username: String = ""
This would automatically sync the username
across devices without needing the manual onChange/onReceive. Whether you use a helper library or not, the underlying concept is the same as our manual implementation above.
Security and Privacy Considerations
Before you go syncing everything to iCloud, keep in mind a couple of points on security:
- Not for Sensitive Data: Neither UserDefaults nor NSUbiquitousKeyValueStore are encrypted storage for secrets. They’re fine for preferences and app state, but don’t store passwords, private personal data, or sensitive keys in them. If you need to sync sensitive info (like credentials) across devices, consider using the iCloud Keychain or CloudKit with proper encryption. UserDefaults and iCloud KVS data is stored under the user’s iCloud account and is protected by their Apple ID security, but it isn’t end-to-end encrypted specifically for your app’s data. In short, treat it as semi-private, not ultra-secure.
- User Consent: Using iCloud Key-Value store means data goes through iCloud. Generally, if the user is signed in to iCloud, this just works. But be transparent — if your app is syncing data, it can be good to mention in a privacy policy or settings screen that preferences are synced via iCloud. (For non-sensitive preferences this is rarely an issue, but transparency is always nice.)
- Conflict Handling: In rare cases, if a user has been offline on multiple devices and then comes online, there might be multiple differing updates. The iCloud key-value store will try to merge and ensure all devices end up consistent. Typically, last write wins, but you should design your usage such that conflicts are unlikely or not critical (which is usually true for simple settings like a toggle or a name). Apple specifically advises not to use NSUbiquitousKeyValueStore for “critical to app functioning” data — partly because of these eventual consistency and conflict limitations. For truly critical or concurrent data, CloudKit might be more appropriate.
Conclusion
With NSUbiquitousKeyValueStore, syncing user preferences across devices in a SwiftUI app is surprisingly easy. It’s almost like using UserDefaults, with just a bit of extra wiring to listen for updates. We covered how to save to the iCloud key-value store and respond to changes from other devices, all using SwiftUI-friendly patterns.
For junior and mid-level iOS developers, this tool can dramatically improve user experience — your users won’t have to set things twice. Just remember its purpose: keep it to small, non-sensitive bits of data that enhance continuity. If you stay within its sweet spot (like theme settings, app preferences, game options, etc.), NSUbiquitousKeyValueStore can feel like magic glue between your app installations.
Now go ahead and try it out in your own app. Your users (and their multiple devices) will thank you for the seamless experience!