ICloud Key-Value Store Across Apps Outside the Store

Using iCloud Key-Value Store Across Apps

We wanted to use this for a couple of our macOS apps that are not distributed through the App Store. The first thing that I noted was that in the documentation for NSUbiquitousKeyValueStore, the last paragraph of the Overview says:

"To use this class, you must distribute your app through the App Store or Mac App Store"

I was instantly discouraged by this and so I started looking at the other iCloud functionality to see if it had the same limits. I didn’t see anything in the documentation, nor in the search results that I found online. So I started to look at using that as a possibility. This seemed possible, but overly complicated for a couple of simple values that I wanted to make available. And again, online there were some implications that you needed to have at least one app in an App Store to be able to use the iCloud functionality, so I asked in a developer slack that I am a member of and got some uncertain replies about both iCloud in general and key-value specifically. So I decided to just write two apps and see what happens.

Turns out that it is pretty simple to do and works well enough. In fact, there even is documentation about it, but man was it not easy to find. So here is what you need to do and the links to documents that describe it.

So the first useful thing was a link to the supported capabilities for macOS page that Michael Tsai pointed me at, that said clearly that the key-value store is usable by an app distributed using a Developer ID! That is progress. I have raised a Feedback about the documentation issues (FB12199267), we’ll see if it gets fixed.

The next step is to make sure that your app has the correct capabilities. I added the iCloud capability to the app and checked the Key-value storage item in that section.

Key-Value Capability

To use with multiple apps, you need to ensure that they are all using the same identifier for the key-value store. However, you cannot set that in the UI as you can for the CloudKit Containers. You’ll need to open the entitlements file and edit it directly. Find the iCloud Key-Value Store item and adjust the value to be a common one that you use in all apps. In this document, which I found later on (not when searching – I actually can’t remember where I clicked to get there), they give you details about how to do this (search for “Multiple Apps”) and they recommend using the id of the “main” app to share the info and then set that for all apps.

That can work, but I just designated a specific id (i.e. com.littleknownsoftware.key-values.common). You should just replace the part after the $(TeamIdentifierPrefix) though. I am not 100% sure if it would be a problem to remove it, but it seems like it would. Also you could try using a wildcard there (i.e. com.littleknownsoftware.key-values.*), but I don’t know if that would work or not. It seems unlikely.

I had some issues with the provisioning and signing, but since these will vary for each setup, I am not going to go into that too much. If you normally just use the “Automatically manage signing”, hopefully it will work for you. If not then you need to be sure to manually create the Identifier for the app and the profile in your developer account online, like I did.

Using the Key-Value store is super easy after this stuff is all setup. In your applicationDidFinishLaunching: method, you can setup to receive notifications when things change like this:

	func applicationDidFinishLaunching(_ aNotification: Notification) {
		NotificationCenter.default.addObserver(self, selector: #selector(storeDidChange), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: NSUbiquitousKeyValueStore.default)
		NSUbiquitousKeyValueStore.default.synchronize()
	}

In a very simple case of displaying the values that are they, you will get those notifications in the method below and reload the tableView as follows:

	@objc
	func storeDidChange(_ note: Notification) {
		OperationQueue.main.addOperation {
			tableView.reloadData()
		}
	}

	//	MARK: TableView Delegation
	func numberOfRows(in tableView: NSTableView) -> Int {
		return NSUbiquitousKeyValueStore.default.dictionaryRepresentation.count
	}
	
	func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
		let dict = NSUbiquitousKeyValueStore.default.dictionaryRepresentation
		guard row < dict.count else {
			return nil
		}
		let key = dict.keys.sorted()[row]
		return ["key": key, "value": dict[key]]
	}

Note that the notification will come in on a background thread, so be sure to put your UI actions on the main thread so you don’t crash!

To set values, simple call NSUbiquitousKeyValueStore.default.set("key", forKey: "value").

All your apps will have access to this data, as long as the user is logged into iCloud.