Public CloudKit with CoreData

Patrick Maltagliati
4 min readOct 15, 2020

--

I decided to make a sample app to learn more about encryption and CloudKit with Core Data and I wanted to share what I learned.

The app is a global messaging ecosystem. Each iCloud account generates a public and private key that is used for encryption of messages. The app allows users to see the messages they have sent and received. Any user is allowed to send a message to any other user.

Lets set up CloudKit with Core Data using both a public and private database.

You will device to run the sample app as CloudKit does not work on the simulator.

Full sample app: https://github.com/patmalt/Messanger

Note The CloudKit public database only gives a certain level of performance and refresh rate. Learn more about the limitations before deciding it is right for your use case.

Schema

Private Keys are stored in the private CloudKit database. This keeps the key secret and could allow a user to share the private key between their own devices.

Messages, Public Keys, and Users are stored in the public database. Each message has an user that represents the recipient, a sender, and the encrypted body. The user keeps a display name for the user and a public key.

Both the public and private databases are required to have their own persistent store. Persistent stores are created by describing the store to the coordinator. The descriptions must specify the correct databaseScope and configuration.

Public and private entities are divided among configurations. Ensure the Used with CloudKit checkbox is checked for the public and private configurations. All entities will belong to the Default configuration.

let container = NSPersistentCloudKitContainer(name: "Messanger")let defaultDesctiption = container.persistentStoreDescriptions.firstlet url = defaultDesctiption?.url?.deletingLastPathComponent()
let privateDescription = NSPersistentStoreDescription(url: url!.appendingPathComponent("private.sqlite"))let privateOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "<iCloud Contrainer ID>")privateOptions.databaseScope = .privateprivateDescription.cloudKitContainerOptions = privateOptionsprivateDescription.configuration = "Private"privateDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)privateDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
let publicDescription = NSPersistentStoreDescription(url: url!.appendingPathComponent("public.sqlite"))let publicOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "<iCloud Contrainer ID>")publicOptions.databaseScope = .publicpublicDescription.cloudKitContainerOptions = publicOptionspublicDescription.configuration = "Public"publicDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)publicDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [privateDescription, publicDescription]

Unfortunately, we are unable to have relationships across configurations. So I created a “soft” relationship by storing an identifier on both Public and Private keys entities that link them together.

Upload Schema

Once you have finalized your schema, add the following code to initialize the schema in iCloud. You do not need to call this method unless you have changed your schema since the last time the schema was initialized.

do {try container.initializeCloudKitSchema(options: NSPersistentCloudKitContainerSchemaInitializationOptions())} catch {print(error)}

Update Schema

You must add 2 indices for this to Core Data to work correctly. Each entity needs modifiedAt and recordName as queryable indices.

Interacting with entities

With all the schema set up, all you need to do is interact with your data just like normal. The types will reside in the correct database due to their assigned configuration.

Create objects:

let context = container.newBackgroundContext()context.perform {let newPrivateKey = PrivateKey(context: context)newPrivateKey.key = key.rawRepresentationnewPrivateKey.userRecordId = iCloudUser.userRecordID?.recordNamelet newPublicKey = PublicKey(context: context)newPublicKey.key = key.publicKey.rawRepresentationlet newUser = User(context: context)newUser.recordId = iCloudUser.userRecordID?.recordNamenewUser.messages = []newUser.name = "User name"newUser.publicKey = newPublicKey
let otherUser: User...
let sender: newUser
let message = Message(context: context)message.body = "This is my message".encrypted(privateKey, otherUser.publicKey)message.sent = Date()message.to = otherUsermessage.from = sender

Fetch objects:

import SwiftUIlet inbox: FetchRequest<Message> = FetchRequest(sortDescriptors:  [NSSortDescriptor(keyPath: \Message.sent, ascending: true)], predicate: NSPredicate(format: "to == %@", user), animation: .default)let outbox: FetchRequest<Message> = FetchRequest(sortDescriptors:  [NSSortDescriptor(keyPath: \Message.sent, ascending: true)], predicate: NSPredicate(format: "from == %@", user), animation: .default)

--

--