Migrating existing app from NSPersistentContainer to NSPersistentCloudKitContainer - swift

I have an app that uses local device only CoreData (NSPersistentContainer). I am looking to migrate so the app is compatible with NSPersistentCloudKitContainer. I understand all the CloudKit setup for NSPersistentCloudKitContainer, but how do I migrate the data that is on the player's phones to iCloud? (i.e. how do I migrate existing core data from NSPersistentContainer to NSPersistentCloudKitContainer)?

A good intro how to do this is given in the 2019 WWDC video „Using Core Data With CloudKit“.
The essential points are:
Replace NSPersistentContainer by ist subclass NSPersistentClouKitContainer.
Initialize the iCloud schema using the container function initializeCloudKitSchema (this has to be done only once after the core data model has been set up or changed).
In the iCloud Dashboard, make every custom type (these are the types starting with CD_) queryable.
In the iCloud Dashboard, set the security type of all CD_ record types for all authenticated users to read/write.
Implement history tracking for core data (here are Apple’s suggestions).
In case you have multiple persistent stores (e.g. a local store relevant only to one device, a private store shared with all users with the same Apple ID, and a shared store shared with other users), one way to set up this is the following:
private (set) lazy var persistentContainer: NSPersistentCloudKitContainer! = {
// This app uses 3 stores:
// - A local store that is user-specific,
// - a private store that is synchronized with the iCloud private database, and
// - a shared store that is synchronized with the iCloud shared database.
let persistentStoresLoadedLock = DispatchGroup.init() // Used to wait for loading the persistent stores
// Configure local store
// --------------------------------------------------------------------------------------------------
let appDocumentsDirectory = try! FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
let coreDataLocalURL = appDocumentsDirectory.appendingPathComponent("CoreDataLocal.sqlite")
let localStoreDescription = NSPersistentStoreDescription(url: coreDataLocalURL)
localStoreDescription.configuration = localConfigurationName
// --------------------------------------------------------------------------------------------------
// Create a container that can load the private store as well as CloudKit-backed stores.
let container = NSPersistentCloudKitContainer(name: appName)
assert(container.persistentStoreDescriptions.count == 1, "###\(#function): Failed to retrieve a persistent store description.")
let firstPersistentStoreDescription = container.persistentStoreDescriptions.first!
let storeURL = firstPersistentStoreDescription.url!
let storeURLwithoutLastPathComponent = storeURL.deletingLastPathComponent
// Configure private store
// --------------------------------------------------------------------------------------------------
let privateStoreDescription = firstPersistentStoreDescription
privateStoreDescription.configuration = privateConfigurationName
// The options below have to be set before loadPersistentStores
// Enable history tracking and remote notifications
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
privateStoreDescription.cloudKitContainerOptions!.databaseScope = .private
// --------------------------------------------------------------------------------------------------
// Configure shared store
// --------------------------------------------------------------------------------------------------
let sharedStoreURL = storeURLwithoutLastPathComponent().appendingPathComponent("Shared")
let sharedStoreDescription = NSPersistentStoreDescription(url: sharedStoreURL)
sharedStoreDescription.configuration = sharedConfigurationName
sharedStoreDescription.timeout = firstPersistentStoreDescription.timeout
sharedStoreDescription.type = firstPersistentStoreDescription.type
sharedStoreDescription.isReadOnly = firstPersistentStoreDescription.isReadOnly
sharedStoreDescription.shouldAddStoreAsynchronously = firstPersistentStoreDescription.shouldAddStoreAsynchronously
sharedStoreDescription.shouldInferMappingModelAutomatically = firstPersistentStoreDescription.shouldInferMappingModelAutomatically
sharedStoreDescription.shouldMigrateStoreAutomatically = firstPersistentStoreDescription.shouldMigrateStoreAutomatically
// The options below have to be set before loadPersistentStores
// Enable history tracking and remote notifications
sharedStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
sharedStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
sharedStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions.init(containerIdentifier: "iCloud.com.zeh4soft.shop")
// For sharing see https://developer.apple.com/documentation/cloudkit/shared_records
// and https://medium.com/#adammillers/cksharing-step-by-step-33800c8950d2
sharedStoreDescription.cloudKitContainerOptions!.databaseScope = .shared
// --------------------------------------------------------------------------------------------------
container.persistentStoreDescriptions = [localStoreDescription, privateStoreDescription, sharedStoreDescription]
for _ in 1 ... container.persistentStoreDescriptions.count { persistentStoresLoadedLock.enter() }
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
// The completion handler will be called once for each persistent store that is created.
guard error == nil else {
/*
Apple suggests to replace this implementation with code to handle the error appropriately.
However, there is not really an option to handle it, see <https://stackoverflow.com/a/45801384/1987726>.
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("###\(#function): Failed to load persistent stores: \(error!)")
}
if storeDescription.configuration == self.privateConfigurationName {
/*
Only if the schema has been changed, it has to be re-initialized.
Due to an Apple bug, this can currently (iOS 13) only be done with a .private database!
A re-initialization requires to run the app once using the scheme with the "-initSchema" argument.
After schema init, ensure in the Dashboard:
For every custom type, recordID and modTime must have queryable indexes.
All CD record types must have read/write security type for authenticated users.
Run later always a scheme without the "-initSchema" argument.
*/
if ProcessInfo.processInfo.arguments.contains("-initSchema") {
do {
try container.initializeCloudKitSchema(options: .printSchema)
} catch {
print("-------------------- Could not initialize cloud kit schema --------------------")
}
}
}
persistentStoresLoadedLock.leave() // Called for all stores
})
let waitResult = persistentStoresLoadedLock.wait(timeout: .now() + 100) // Wait for local, private and shared stores loaded
if waitResult != .success { fatalError("Timeout while loading persistent stores") }
return container
} ()
EDIT:
privateConfigurationName, as well as sharedConfigurationName are Strings:
let privateConfigurationName = "Private"
let sharedConfigurationName = "Shared"
and Private and Shared are used as Configuration names in the Coredata model, e.g.:
One has to assign there the entities to the persistent store(s).
A warning:
I you assign the same entity to multiple persistent stores, a save of a managed context will store it in all assigned stores, except you assign a specific store, see this post.
Likewise, a fetch will fetch a record from all persistent stores the entity is assigned to, except you set affectedStores in the fetch request, see the docs.

I did the following :
Replaced NSPersistentContainer by NSPersistentCloudKitContainer
And added enabled history tracking
container = NSPersistentCloudKitContainer(name: "myApp") // <<<<< this
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber,
forKey: NSPersistentHistoryTrackingKey) // <<<<< this
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(...)
EDIT: Well, I talked too quick. It does not work :(
I found that people have little tricks here https://developer.apple.com/forums/thread/120328
(for example edit the items to trigger a sync, or to manually transfer each object as said explained here https://medium.com/#dmitrydeplov/coredata-cloudkit-integration-for-a-live-app-57b6cfda84ad)
But there is no actual answers...

Related

Couldn't initialize CloudKit schema because no stores in the coordinator are configured to use CloudKit

I'm getting this error when trying to initialize the Cloudkit schema:
Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=Couldn't initialize CloudKit schema because no stores in the coordinator are configured to use CloudKit: ()}
This is my code:
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentCloudKitContainer(name: self.objectModelName)
// Create a store description for a CloudKit-backed local store
let cloudStoreLocation = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("iCloud.sqlite")
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudStoreLocation)
cloudStoreDescription.configuration = "iCloud"
// Set the container options on the cloud store
cloudStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.myname.app")
// Update the container's list of store descriptions
container.persistentStoreDescriptions = [
cloudStoreDescription
]
do {
try container.initializeCloudKitSchema()
} catch {
print(error)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
This is my: project configuration
This is my: data model schema
Can you anyone help me spot what I'm doing wrong?
Thanks!
To whom may come across this later. I had the same error and the issue was same as the sample code above.
I called
container.initializeCloudKitSchema()
before calling
container.loadPersistentStores
You will need to instead call container.initializeCloudKitSchema after calling container.loadPersistentStores
For god's sake, I just had to sign-in with my AppleID into the iPhone's Settings, and also enable iCloud Drive in Settings > My Name > iCloud in the same device.
This wasn't explicitly mentioned in the tutorial on Apple's developer docs.

How to deal with concurrency on core data

I've been wandering around google, stackoverflow and internet trying to understand how to work with core data and deal with the concurrency.
Consider that we have 2 tables, Events and Rooms.
An Event can have 1+ Rooms.
FunctionA - AddEvent
FunctionB - AddRoom
FunctionC - SearchRoom -> returns RoomEntity or nil
My problem, I keep getting these errors
Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x10a507160) for NSManagedObject (0x1092f00c0) with objectID '0xd000000000040000 <x-coredata://A34C65BD-F9F0-4CCC-A9FB-1B1F5E48C70E/Rooms/p1>' with oldVersion = 116 and newVersion = 124 and old object snapshot = {\n location = Lisboa;\n name = \"\\U00cdndico LX\";\n} and new cached row = {\n location = Lisboa;\n name = \"\\U00cdndico LX\";\n}"
Notice the information of the Rooms is equal
my approach is the following.
1- I call the webservice once ( it brings a json with data of 3 types of Events ) These 3 all have the same json structure and share the same managedObjectContext passed by parameter
2- I create a managedObject
var managedObjectContext: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext = persistentContainer.viewContext
managedObjectContext.parent?.mergePolicy = NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType
3-
managedObjectContext.perform(
{
do
{
try self.deleteAllEventsFromDb()
FunctionA(eventList, managedObjectContext) -> save
FunctionA(eventList2, managedObjectContext) -> save
FunctionA(eventList3, managedObjectContext) -> save
self.DatabaseDispatchGroup.enter()
try managedObjectContext.save()
self.DatabaseDispatchGroup.leave()
completion(Result.Success(true))
}
catch let error as NSError
{
print("Could not save. \(error), \(error.userInfo)")
completion(Result.Success(false))
}
})
4- For each Event I execute the same FunctionA to create and save the data in database (managedObjectContext.insert(eventEntity)) . This will work over several tables but lets only consider Events and Rooms(FunctionB).
5- FunctionA contains functionB. Function B search for an existing Room(FunctionC->returns entity?) if it doesn't exists(nil), it creates the entity ( should I save here? )
6- If a Room exists, gets the entity and tries to update the data
Not sure if its making any difference but when I save I do these saves I do it between a dispatchGroup
DatabaseDispatchGroup.enter()
try managedObjectContext.save()
DatabaseDispatchGroup.leave()
I was using a static managedObjectContext which was used for all the database requests but now I decided to create a objectContext per function which accesses the database.
I do keep the same persistentContainer and the same DispatchGroup for all requests
private override init() {
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores() { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
It seems to me that my problem is that I am Storing the changes in memory, and always doing updates over the initial data meaning that when I execute save() the context the data is not updated for the next operation?
How/when am I suppose to execute the save function?
Thank you
Once context is being saved, global notification is being posted: ContextDidSave notification.
When using multiple contexts (and not using parent-child approach) you should use this notification and either:
Re-fetch/refresh data in case you need to update view or perform some operation on new data set (using either fetch request or refreshObjects: API).
Merge changes to other contexts (remember about thread confinement! do that only on proper context queues). (merge doc)
There are many articles about it, check for instance this tutorial
and documentation

Correct Way to Initialize New CKRecord

I am using Realm for my local cache in a Mac app, and CloudKit for sync. Up until now, I have been initializing CKRecord objects like this:
let record = CKRecord(recordType: "Workspace", recordID: CKRecordID(recordName: workspace.recordName, zoneID: "..."))
The object workspace is my locally cached object and its recordName matches the CKRecord's recordName.
I recently learned about encodeSystemFields and that I need to store the record's metadata in my local cache. But as far as I can tell, the only way to initialize an object with that meta data is like this:
let coder = NSKeyedUnarchiver(forReadingWith: object.recordData!)
coder.requiresSecureCoding = true
let record = CKRecord(coder: coder)
coder.finishDecoding()
But if I initialize my CKRecord with coder, how can I specify my recordID and zoneID?
You're only going to be using that initializer if you already have a record (you're modifying or deleting). The encoded system fields contain that information so you don't need to specify that directly. If you're creating a new record you will use one of the other initializers to generate those specifically, for example like so.
So for example in my app when making a record to sync with iCloud I just check for the existence of metadata, and if it is there I use CKRecord(coder:), otherwise CKRecord(recordType: recordID:) like so:
if let ckMetaData = object.value(forKey: Schema.GenericFieldNames.ckMetaData) as? Data
{
// MetaData exists
if self.debug
{
print("🗻 RecordFromObject \(self.objectName) We have Metadata. This will update an existing record in iCloud")
}
let unarchiver = NSKeyedUnarchiver(forReadingWith: ckMetaData)
unarchiver.requiresSecureCoding = true
self.outputRecord = CKRecord(coder: unarchiver)
}
else
{
// No MetaData
if self.debug
{
print("🗻 RecordFromObject \(self.objectName) We have no Metadata. This record will be new to iCloud!")
}
let objectID = object.objectID.uriRepresentation().absoluteString
let recordID = CKRecordID(recordName: objectID, zoneID: self.inputRecordZoneID)
self.outputRecord = CKRecord(recordType: object.entity.managedObjectClassName, recordID: recordID)
}

Realm sync with Realm Object Server

Pre-Condition : I have 10 Dogs stored in a Realm Server
Is there a way to know when the results are ready?
let usernameCredentials = SyncCredentials.usernamePassword(username: email, password: pass)
SyncUser.logIn(with: usernameCredentials,server: Utils.sharedInstance.serverURL) { user, error in
if error != nil {
// handle error
} else {
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user!, realmURL: Utils.sharedInstance.syncServerURL))
let realm = try! Realm(configuration : config)
let dogs = realm.objects(Dog.self)
print("I have : \(dogs.count) dogs")
// dogs count is 0 here
// ..............
// some time later i have the 10 Dogs
}
}
Unfortunately not at the moment. Realm's sync APIs and semantics right now are best suited for incremental sync use cases, but we're working on two features that should help address the use case you've shared.
The first is a "Download Realm" API which only makes the Realm available once its entire contents have been downloaded.
The second is "sync progress notifications", where you can register a progress update block to fire with information about how much 1) local data needs to be synced up and 2) remote data needs to be synced down.

Can't save or load objects

I am new to Realm and its the first time I am using it. I followed every step from the guide and its inserted in my project just fine. I created a model and a function to insert the object into the realm database.
Somehow I keep getting errors. Here is what I do.
my function
do {
let realm = try Realm()
let proposition = Proposition()
proposition.name = (currentProposition.name)
proposition.energyType = (currentProposition.energyType)
proposition.lifetime = (currentProposition.lifetime)
proposition.saving = (currentProposition.saving)
proposition.investing = (currentProposition.investing)
if let _ = propositionsArray.indexOf(proposition) {
try! realm.write {
realm.delete(proposition)
loadPropositions()
}
} else {
try! realm.write {
realm.add(proposition)
loadPropositions()
}
}
} catch let error as NSError {
print("Add proposition error \(error)")
}
Here is my model
import RealmSwift
import Foundation
class Proposition : Object {
dynamic var name: String = ""
dynamic var energyType: String = ""
dynamic var lifetime = 0
dynamic var saving = 0
dynamic var investing = 0
}
Somehow I keep getting the following error
Can someone tell me what I am doing wrong?
The errors you're seeing indicate that the data model defined by your application does not match the data model of the Realm you're opening. This is usually due to changing your data model. In this case, the errors mention that you've added the lifetime, saving, and investing properties, and changed name and energyType to be non-nullable.
There are two ways to accommodate changes to your data model:
If you're in early development and don't need to support your old data model, you can simply remove the Realm files and start over with empty data.
You can perform a migration to have Realm update the data model of the Realm file. See the Migrations section of the Realm documentation for information about how to perform a migration.