Back to blog

iCloud Key-Value Store with NSUbiquitousKeyValueStore


Aasif Khan
By Aasif Khan | Last Updated on June 28th, 2024 9:07 pm | 4-min read

Ubi–what!? With the NSUbiquitousKeyValueStore you can sync key-value store data across iOS and Mac devices, via iCloud. It’s perfect for persisting user preferences to iCloud, and it’s simple to set up.

In this tutorial you’ll learn:

  • What a key-value store is
  • How NSUbiquitousKeyValueStore is different than UserDefaults
  • How to read from and write to NSUbiquitousKeyValueStore
  • And how to sync data between iOS devices

What’s A Key-Value Store?

A key-value store is a container that stores data items. Every data item has a key and a value. You can read a value from the key-value store by using its associated key.

The simplest key-value store is a dictionary or array, where values are associated with keys and indices respectively. You can compare it to an address book, where you look up a person’s address (a value) by using their name (a key).

Let’s look at an example:

let kvs = [
“name”: “John Appleseed”,
“address”: “1 Infinite Loop Cupertino, CA 95014”,
“country”: “United States”,
“planet”: “Earth”,
“sector”: “Sector ZZ9 Plural Z Alpha”
]

print(“sector = \(kvs[“sector”]!)”)

The dictionary kvs has five key-value pairs, such as key “planet” has value “Earth”. In the above example, the keys and values of kvs both are of type String.

On the last line of the example, we’re printing out the value of kvs[“sector”] with Swift’s subscript syntax. It will print out the value for key “sector”, i.e. Sector ZZ9 Plural Z Alpha.

Key-value stores are important in software development and apps, because they provide a simple way to store “flat” data. Unlike relational databases like MySQL, you can’t use a key-value store to organize relationships between data entries.

Associating keys with values in software development isn’t particularly novel or exciting. What makes key-value stores so interesting is that they come with a number of additional features, such as synchronization, distribution across devices, high-performance lookups, and even clustering for high-availability.

Plenty of key-value store tools exist outside of iOS development. Take Redis for example, which is an “in-memory data structure store.” It has replication and clustering, supports many different data formats, and it’s an all-round beast when it comes to web-based key-value storage.

In this tutorial, we’re going to work with a key-value store that’s persisted via iCloud. It’s perfect to read and write key-value data, and sync it across iCloud-enabled devices. And it persists data between app installs, which makes it particularly handy for configuring apps.

Writing Data To iCloud Key-Value Store

The class we’re working with here is called NSUbiquitousKeyValueStore. It’s a bit of a mouth full, so let’s break it down:

  • “NS” stands for NeXTSTEP, which is the name of the operating system that ultimately found its way into macOS and iOS
  • “Ubiquitous” is a fancy word that means “present everywhere” – quite a good name for a key-value store that’s synced with iCloud!
  • “Key Value Store”, we’ve talked about that, and for short we’ll often call it KVS

The NSUbiquitousKeyValueStore is very similar to UserDefaults. In fact, UserDefaults is a key-value store too. The big difference is that UserDefaults only stores data locally in your app, and won’t sync it across devices.

Just as UserDefaults, the NSUbiquitousKeyValueStore has a .default class property that we can use to read from and write to the default key-value store.

Writing to the key-value store is as simple as:

NSUbiquitousKeyValueStore.default.set(“Earth”, forKey: “planet”)

The above code sets a value for a particular key. The iCloud key-value store can handle several data types, such as Bool, Data, Dictionary, Array, String, Double, Date and Int.

There are some limits to the size of data you can store:

  • A maximum, overall key-value store size of 1 MB (per user)
  • One key-value pair can’t be larger than 1 MB
  • You can’t store more than 1024 key-value pairs
  • A key can’t be larger than 64 bytes using UTF-8 encoding

If you’re only using the iCloud key-value store to save simple text-based configuration values, you won’t exceed these limits. If you’re looking to store large amounts of data, i.e. a user’s content, or images, don’t use NSUbiquitousKeyValueStore.

To use NSUbiquitousKeyValueStore, you’ll need to distribute your app through the iOS or Mac App Store. You must also set the com.apple.developer.ubiquity-kvstore-identifier entitlement in your app’s Entitlements. You can find your app’s entitlements in Xcode, by going to Project Settings -> Capabilities. Enable the iCloud capability and check Key-value storage. You might need to enable iCloud for your App ID too, by going to Certificates, Identifiers & Profiles on developer.apple.com/account.

Fun Fact: UTF-8 is a multi-byte encoding for text. You can compare encoding to using a dictionary to map byte values to human-readable characters. UTF-8 takes 1 to 4 bytes per character, with the first 128 ASCII characters only needing one byte. In short, you can fit 64 alphanumeric characters in 64 bytes of a UTF-8 string. You’ll need 2 bytes for diacritic characters (accents), and languages like Greek and Cyrillic. You’ll need 3 bytes for most Japanese, Chinese and Korean characters. And last but not least: 4 bytes for emoji… In short, if you use simple strings like “address”, you’ve got plenty of space.

Reading Data From iCloud Key-Value Store

Reading data from the iCloud key-value store is simple, too. Like this:

let sector = NSUbiquitousKeyValueStore.default.string(forKey: “sector”)
print(sector)
// Output: Sector ZZ9 Plural Z Alpha

Unlike the set(_:forKey:) function, the functions to read data from the key-value store use the type of the value explicitly in the function name. So, you can use these functions to read different types from the key-value store:

  • array(forKey:) for arrays (returns optional)
  • bool(forKey:) for booleans (returns false if key doesn’t exist)
  • data(forKey:) for Data objects (returns optional)
  • dictionary(forKey:) for dictionaries (returns optional)
  • double(forKey:) for Double values (returns 0 if key doesn’t exist)
  • longLong(forKey:) for integers (returns 0 if key doesn’t exist)
  • object(forKey:) for objects (returns Any?)
  • string(forKey:) for strings (returns optional)

Most of the functions above return optionals. If a key doesn’t exist, the return value is nil. Notable exceptions are bool(forKey:), double(forKey:) and longLong(forKey:).

If the function returns an optional, you can use the nil-coalescing operator ?? to provide a default value:

let url = NSUbiquitousKeyValueStore.default.string(forKey: “kAPIURL”) ?? “https://example.com/api/json”

And you can also use optional binding to validate data, like this:

if let options = NSUbiquitousKeyValueStore.default.array(forKey: “kOptions”) {
controller.setOptions(options)
} else {
controller.setOptions([ “optionA”: true, “optionB”: false ])
}

Removing key-value pairs is as simple as:

NSUbiquitousKeyValueStore.default.removeObject(forKey: “sector”)

Depending on the architecture of your app, it might be smart to create a separate API or interface to interact with NSUbiquitousKeyValueStore.

What you don’t want, is sprinkling code that reads from and writes to the key-value store throughout your view controllers, for instance. A separate API will also help you migrate to another key-value store tool, if needed.

What’s also smart, is to create central structures for the key names you use in NSUbiquitousKeyValueStore, or UserDefaults. Instead of working with strings directly, you organize any key strings you might use in your app in one nested struct. Like this:

struct Constants
{
struct color {
static let blue = UIColor.hex(“#01133A”)
// etc.
}

struct keys
{
static let kUseData = “app_useData”
static let kLastProject = “app_lastProject”
static let kLastUpdated = “app_lastUpdated”
}
}

When you want to read from or write to the key-value store, using these constants will save you from accidentally making typos. Like this:

let syncEnabled = NSUbiquitousKeyValueStore.default.bool(forKey: Constants.keys.kUseData)

Make a typo in kUseDta and your code won’t compile, whereas for “kUseDta” you’re going to pull your hairs out searching for that typo…

Responding To Data Changes From iCloud

The difference between NSUbiquitousKeyValueStore and UserDefaults is that the ubiquitous key-value store (what a name!) synchronizes to iCloud. This essentially means that iOS will upload the key-value store to iCloud when you write something to it, and download that same key-value store into another install of the same app.

The data in the key-value store is persisted between app installs – so it’ll be there even if you uninstall the app – and it’s synced between devices. You can even share the data store between iOS and Mac apps that have the same identifier.

Before we find out how synchronization works, what would you use a synced key-value store for? Some ideas:

  • User-specific data that isn’t persisted in another database, such as a user’s name, address, phone number or email address
  • App-specific configuration values, like feature flags and a user’s in-app preferences
  • The state of an app, for state restoration and synchronization between app installs, i.e. “start again on device B where you left on device A”

Just like UserDefaults, you don’t have to actively synchronize the key-value store on the device. Here’s how that works:

  • You write a key-value pair to NSUbiquitousKeyValueStore
  • This key-value pair is stored in-memory as the app runs
  • At some point the data is persisted to disk, or you can call synchronize() manually
  • At some point the data is sent to iCloud, where it is stored and pushed to other devices

Pushing to iCloud goes automatically. You can call synchronize() explicitly to sync the in-memory key-value store with the data on the disk. Like this:

NSUbiquitousKeyValueStore.default.synchronize()

You can also observe changes to the iCloud data store in your app. Whenever data changes, a notification is sent to a function you specify, which includes the keys that have been changed. This mechanism uses NotificationCenter.

Here’s how you register for those changes:

NotificationCenter.default.addObserver(self, selector: #selector(onUbiquitousKeyValueStoreDidChangeExternally(notification:)), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: NSUbiquitousKeyValueStore.default)

It’s a bit of a beast. Here’s what it does:

  • You add self as the observer of the .didChangeExternallyNotification notification
  • The last parameter of addObserver(…) indicates that you only want to receive notifications from the .default key-value store
  • When a notification is observed, the function onUbiquitousKeyValueStoreDidChangeExternally(notification:) is called

And here’s that function:

@objc func onUbiquitousKeyValueStoreDidChangeExternally(notification:Notification)
{
// Do something…
}

Inside that function you can use the notification parameter to get information about the pushed key-value data, via its userInfo property. Such as:

  • NSUbiquitousKeyValueStoreChangeReasonKey to get the reason of the data change
  • NSUbiquitousKeyValueStoreChangedKeysKey to get the keys of data that’s changed

You can also get the key-value data store object that’s changed, via notification.object. This is especially helpful if you’re working with multiple NSUbiquitousKeyValueStore objects.

Further Reading

That’s all there is to it! Reading and writing data with the iCloud data store is surprisingly simple. The NSUbiquitousKeyValueStore is ideal for syncing key-value store data between iOS devices – and now you know exactly how it works.


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts