In this article, we’ll look at one of the most common scenarios in Core Data with CloudKit applications — synchronizing your local database to your iCloud private database. We will start at several levels:
- Direct support in new projects
Core Data with CloudKit
- Create synchronizable
Model
Matters needing attention - Existing projects
Core Date
addHost in CloudKit
support - Selective synchronization of data
The development environment used in this article is Xcode 12.5. For the concept of private databases, see Core Data with CloudKit (1) — Basics. To actually use this article, you need an Apple Developer Program account.
Quick guide
To enable Core Data with CloudKit in your application, just take the following steps:
- use
NSPersistentCloudKitContainer
- in
The project Target
theSigning&Capablities
addCloudKit
support - Create or specify for the project
CloudKit container
- in
The project Target
theSigning&Capablities
addbackground
support - configuration
NSPersistentStoreDescription
As well asviewContext
- check
Data Model
Whether the synchronization requirements are met
Direct support for Core Data with CloudKit in new projects
In recent years, Apple has continuously improved the Core Data template for Xcode. It is the easiest way to start a project that supports Core Data with CloudKit by directly using the built-in template.
Create a new Xcode project
Create a new project, select Use Core Data and Host in CloudKit (earlier version: Use CloudKit) in the project Settings interface, and set the development Team.
After setting the save address, Xcode will use the preset templates to generate project documents for you with Core Data with CloudKit support.
Xcode may alert a new project to errors in the code, but if you’re worried, just Build the project and cancel the error.
Next, follow the quick guide step by step.
Set the PersistentCloudKitContainer
Persistence. Swift is the Core Data Stack created by the official template. Because at the time of creating the project have chosen Host in CloudKit, so template code is used directly replace NSPersistentContianer NSPersistentCloudKitContianer, without modification.
let container: NSPersistentCloudKitContainer
Copy the code
Enable CloudKit
Click on the corresponding Target in the project and select Singing&Capabilities. Click +Capability to find icloud and add CloudKit support.
Check the CloudKit. Click + and enter the CloudKit Container name. Xcode will automatically add iCloud in front of your CloutKit Container name. The container name is usually a reverse domain name and does not need to be the same as the project name or BundleID. If the developer team is not configured, the Container cannot be created.
In the addCloudKit
Once supported, Xcode will automatically add it for youPush Notifications
Functions, reasons we talked about in the last post.
Enabling background Notifications
Continue to click on +Capability, search for Background and add, check Remote Notifications
This feature allows your app to respond to silent notifications when data content changes in the cloud.
Configuration NSPersistentStoreDescription and viewContext
If you look at the.xcDatamodeld file in your current project, you can see that you have only one Default configuration, Default, in the CONFIGURATIONS file.
If the developer does not customize the Configuration in the Data Model Editor, if Used with CloudKit is selected, Core Data sets cloudKitContainerOptions using the selected Cloudkit Container. So in the current Persistence. Swift code, we don’t need to NSPersistentStoreDescription do any additional setup (we will introduce how to set up in the back of the chapter NSPersistentStoreDescription `).
Configure the context in persistence.swift as follows:
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
.
fatalError("Unresolved error \(error).\(error.userInfo)")}})// Add the following code
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("Failed to pin viewContext to the current generation:\(error)")}Copy the code
Container. ViewContext. AutomaticallyMergesChangesFromParent = true to view context automatically merge server synchronization (import) to the data. Using the @ FetchRequest or NSFetchedResultsController view data changes can be timely reflected in the UI.
Container. ViewContext. MergePolicy = NSMergeByPropertyObjectTrumpMergePolicy set merge conflicts strategy. If this property is not set, Core Data will default to NSErrorMergePolicy as a conflict resolution policy, which will result in iCloud Data not being properly merged into the local database.
Core Data presets four merge conflict policies, which are:
-
NSMergeByPropertyStoreTrumpMergePolicy
In a attribut-by-attribute comparison, if both persistent and in-memory data change and conflict, the persistent data wins
-
NSMergeByPropertyObjectTrumpMergePolicy
In a attribut-by-attribute comparison, if both persistent data and in-memory data change and conflict, the in-memory data wins
-
NSOverwriteMergePolicy
Memory always wins
-
NSRollbackMergePolicy
Persistent data always wins
For the Core Data with CloudKit such usage scenarios, will usually choose NSMergeByPropertyObjectTrumpMergePolicy.
SetQueryGenerationFrom (.current) This has only recently appeared in Apple’s documentation and routines. The goal is to avoid possible instability during data import due to changes in the data generated by the application and inconsistencies in the imported data. Although I’ve rarely seen this in my more than two years of use, I recommend adding context snapshot locking to your code to improve stability.
Until Xcode 13 Beta4, Apple has not added a context setting to the pre-set Core Data with CloudKit template, which makes importing Data using the original template not as good as expected and not as friendly to beginners.
Check whether the Data Model meets the synchronization requirements
The Data Model for the template project is very simple, with only one Entity and one Attribute, so there is no need to adjust it at the moment. The rules that apply to Data Model synchronization are described in detail in the next section.
Modify the ContentView. Swift
Note: ContentView.swift generated by the template is incomplete and needs to be modified to display correctly.
var body: some View {
NavigationView { / / add NavigationView
List {
ForEach(items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
.onDelete(perform: deleteItems)
}
.toolbar {
HStack { / / add HStack
EditButton(a)Button(action: addItem) {
Label("Add Item", systemImage: "plus")}}}}}Copy the code
After the change, the Toolbar button can now be displayed normally.
At this point, we have completed a project that supports Core Data with CloudKit.
run
If you set up and log in to the same iCloud account on the simulator or the real machine, only the same account can access the same iCloud private database.
Below is a GIF of an Airplay screen and an emulator.
After the video is edited, the data synchronization time is usually about 15-20 seconds.
Operations performed from the simulator (add, delete) are usually reflected to the real machine in about 15-20 seconds; Operations performed from the real machine, however, need to be switched to the background and back to the foreground in order to be reflected in the simulator (because the simulator does not support silent notification responses). If you are testing between two simulators, you need to do something similar on both sides.
The Apple documentation describes the synchronization + distribution time as less than 1 minute, but in practice it is usually around 10-30 seconds. Supports batch data update without worrying about the efficiency of large data update.
When the data changes, the console generates a lot of debugging information, and there will be more on debugging in a later article.
Considerations for creating a synchronous Model
To perfectly pass records between Core Data and CloudKit databases, it is a good idea to have some understanding of the Data structure types of both parties. For details, see Core Data with CloudKit (I) — Basics.
CloudKit Schema does not support all the features and configurations of the Core Data Model, so be aware of the following limitations when designing Core Data projects that can be synchronized and make sure you create a compatible Data Model.
Enitites
CloudKit Sechma
Does not supportCore Data
The only restriction (Unique constraints
)
Unique Constraints in Core Data requires support from SQLite. CloudKit itself is not a relational database, so the lack of support is not surprising.
CREATE UNIQUE INDEX Z_Movie_UNIQUE_color_colors ON ZMOVIE (ZCOLOR COLLATE BINARY ASC.ZCOLORS COLLATE BINARY ASC)
Copy the code
Attributes
- There can be no
The optional value
againNo default value
Properties. Allowed: Optional, default, Optional + Default
The attribute in the figure above is not Optional and has no Default Value, which is incompatible. Xcode will report an error.
- Does not support
Undefined
type
Relationships
- All relationships must be set to optional (
Optional
) - All relationships must be in reverse (
Invers
Relationship between) - Does not support
Deny
Delete rule for
CloudKit also has an object that is similar to the Core Data relational type — CKReference. However, this object can only correspond to a maximum of 750 records, which cannot meet the needs of most Core Data application scenarios. CloudKit converts the relationship of Core Data into Record Name (in the form of UUID string) to correspond to each Record. This results in CloudKit probably not storing relationship changes atomically, thus placing tighter restrictions on the definition of relationships.
Most of the relationships defined in Core Data on a day-to-day basis still meet these requirements.
Configurations
- Entity (
Entity
) shall not be configured with other (Configuration
)relationship
I was confused by this restriction in the official documentation, because even when network synchronization is not used, developers usually do not establish a relationship between two entities in the Configuration. If you need to make a connection, it is common to create touchProperties.
After CloudKit synchronization is enabled, Xcode will alert developers with an error if the Model does not meet the synchronization compatibility criteria. When you change an existing project to support Core Data with CloudKit, you may need to make some changes to the code.
Add Host in CloudKit support to existing Core Data projects
With the foundation of the template project, it is easy to upgrade Core Data projects to support Core Data with CloudKit:
- use
NSPersistentCloudKitContainer
replaceNSPersistentContainer
- add
CloudKit
,background
Function and addCloudKit container
- Configuration context
Here are two caveats:
CloudKit container
Unable to certification
When you add a CloudKit Container, sometimes authentication fails. This almost certainly happens when you add a container that has already been created.
CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromPartialError:forStore:inMonitor:]block_invoke(1943): <NSCloudKitMirroringDelegate: 0x282430000>: Found unknown error as part of a partial failure: <CKError 0x28112d500: "Permission Failure" (10/2007); server message = "Invalid bundle ID for container"; uuid = ; container ID = "iCloud.Appname">
Copy the code
The solution is to log in to developer accounts ->Certificates,Identifiers&Profiles->Identifiers App IDs, select the corresponding BundleID, configure iCloud, click Edit and reconfigure Container.
Use a customNSPersistentStoreDescription
Some developers like to customize NSPersistentDescription (even if there is only one Configuration). In this case, you need to explicitly set cloudKitContainerOptions for NSPersistentDescription, for example:
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudStoreLocation)
cloudStoreDescription.configuration = "Cloud"
cloudStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "your.containerID")
Copy the code
Network synchronization works even if you do not set Configuration in the Model Editor to Used with CloudKit
Selective synchronization of data
In practice, there are certain scenarios where we want to synchronize data selectively. By defining multiple configurations in the Data Model Editor, you can help control Data synchronization.
Configuring a Configuration is as simple as dragging an Entity into it.
Place different Enitity in different configurations
Consider the following scenario where we have an Entity, a Catch, that acts as a local data cache and the data in it does not need to be synchronized to iCloud.
Apple’s official documentation and other materials discussing Configuration are mostly for situations like this
We create two Configuration:
- Local –
Catch
- Cloud — Anything else that needs to be synchronized
Entities
Take a code like this:
let cloudURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
.appendingPathComponent("cloud.sqlite")
let localURL = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first!
.appendingPathComponent("local.sqlite")
let cloudDesc = NSPersistentStoreDescription(url: cloudURL)
cloudDesc.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "your.cloudKit.container")
cloudDesc.configuration = "cloud"
let localDesc = NSPersistentStoreDescription(url: localURL)
localDesc.configuration = "local"
container.persistentStoreDescriptions = [cloudDesc,localDesc]
Copy the code
Only Entities in the Configuration Cloud can be synchronized to iCloud.
We can’t be in the straddleConfiguration
theEntity
Created betweenrelationship
It can be used if necessaryFetched Preoperties
To achieve a limited approximation
The same Entity is placed in different configurations
If you want to synchronize (partially synchronize) the data of an Entity, you can use the following scenario.
The scenario is as follows: Let’s say you have an Entity, Movie, and for whatever reason, you only want to synchronize part of it.
-
Add an Attribute to Movie — local:Bool (true for local data, false for synchronous data)
-
Create two configurations — Cloud and Local, and add Moive to both
-
Adoption and the same code above, add two Description in NSPersistentCloudKitContainer
When the NSPersistentCoordinator fetches a Movie, the NSPersistentCoordinator automatically merges and processes the Moive records in the two stores. However, when writing a Movie instance, the coordinator will only write the instance to the Description that contains Movie first, so you need to be careful about the order in which you add it.
Such as container. PersistentStoreDescriptions = [cloudDesc localDesc], in the container. The viewContext in the new Movie will be written to the cloud. Sqlite
-
Create an NSPersistentContainer and call it localContainer, just localDesc
-
Enable Persistent History Tracking on localDesc
-
Use localContainer to create a context to write to the Movie instance (the instance will only be saved locally without network synchronization)
-
Processing NSPersistentStoreRemoteChange notice, will write to the data from localContainer viewContext incorporated into the container
I haven’t found any data to explain why the coordinator can merge multiple queriesStore
In theThe sameEntity
, but it does achieve the desired results in actual use.
The above solution requires the use of Persistent History Tracking. For more information, see my article using Persistent History Tracking in CoreData.
conclusion
In this article, we look at how to implement synchronization of your local database to your iCloud private database.
In the next article, let’s look at how to use the CloudKit dashboard. Another way to think about Core Data with CloudKit.
This article originally appeared on my personal blog, Swift Notepad.