Populating parent Reference (List) in CloudKit with child recordName when creating a child record - swift

I'm trying to build additional functionality beyond that included in a cloudkit tutorial (https://www.raywenderlich.com/4878052-cloudkit-tutorial-getting-started) and have gotten to the point where I have a working app to view Establishments (restaurants) and Notes about them - where I've created the data in the CloudKit dashboard and pre-populated the Notes' recordName(s) in the Establishment's 'notes' Reference (List).
I'm now extending the app to allow the user to add notes and at a point where I'm able to create a new Note, and populate the Establishment's recordName (establishing the parent entity within the child record) with this:
let noteRecord = CKRecord(recordType: "Note")
noteRecord["text"] = noteText.text //grabbed from a UIAlert
if self.establishment?.name != nil { //If the establishment exists
let reference = CKRecord.Reference(recordID: self.establishment!.id, action: .deleteSelf)
noteRecord["establishment"] = reference as! CKRecordValue
}
//works fine, saves the new note as expected with the right recordName for the establishment
CKContainer.default().privateCloudDatabase.save(noteRecord) { [unowned self] record, error in
DispatchQueue.main.async {
if let error = error {
} else {
}
}
}
Now the issue I have is how to grab the recordName of this newly saved Note and append it to the Reference (list) of the Establishment.
The reason being that the way the app was built in the tutorial - when getting all the Notes for an Establishment - it uses the Reference (List) of the establishment. If folks think this is unnecessary from a data structure and just having the reference back to the parent on the child is enough, I'd be interested in the pros/cons of the approaches. As you can tell, I'm just learning!
Any ideas? Thank you!!
Please let me know if more detail would be useful
Data model for Notes - what matters is: "text" = String of the note and "establishment" = reference that holds the Establishment recordName
Data model for Establishment - what matters is "notes" which is a Reference (List) of recordNames of Note items.
GitHub repository here as well: https://github.com/SteveBlackUK/BabiFudTutorial/blob/master/BabiFud/View%20Controllers/NotesTableViewController.swift < this is the view controller I'm working on

While I still have an open question about whether you should store the parent child references in both the parent and the child (probably)...I was finally able to figure out how to append the reference to the child back in the parent record.
Reminder: we are creating a Note about a location (an Establishment), and the goal is to store the reference to the note in the Reference (list) within the Establishment in CloudKit.
//first, make sure we even have an establishment
if self.establishment?.name != nil {
//get the array of note references for the Establishment that we want to update
let noteReferences = CKRecord.Reference(record: noteRecord, action: CKRecord_Reference_Action.deleteSelf)
//store the current array of note references into notesForEstablishment
self.notesForEstablishment = self.establishment?.noteRecords
if self.notesForEstablishment != nil {
if !self.notesForEstablishment!.contains(noteReferences) {
self.notesForEstablishment!.append(noteReferences)
}
} else {
self.notesForEstablishment = [noteReferences]
self.establishment?.noteRecords = self.notesForEstablishment
}
// get the record ID for the current establishment
let establishmentRecordID = self.establishment?.id
//then fetch the establishment from CK
CKContainer.default().publicCloudDatabase.fetch(withRecordID: establishmentRecordID!) { updatedRecord, error in
if let error = error {
print("error handling to come: \(error.localizedDescription)")
} else {
// then update the record and place the array that now holds the reference to the new note into the "notes" Reference (list) in CK
updatedRecord!.setObject(self.notesForEstablishment as __CKRecordObjCValue?, forKey: "notes")
CKContainer.default().publicCloudDatabase.save(updatedRecord!) { savedRecord, error in
}
}
}
}
It could probably be much cleaner code - and any feedback welcome, but it works!

Related

Ambiguous reference to member 'save(_:completionHandler:)' with CloudKit save attempt

I'm trying to save back to CloudKit after updating a reference list and getting the error on the first line of this code block.
Error: Ambiguous reference to member 'save(_:completionHandler:)'
CKContainer.default().publicCloudDatabase.save(establishment) { [unowned self] record, error in
DispatchQueue.main.async {
if let error = error {
print("error handling to come")
} else {
print("success")
}
}
}
This sits within a function where the user going to follow a given location (Establishment). We're taking the existing establishment, and its record of followers, checking to see if the selected user is in it, and appending them to the list if not (or creating it if the list of followers is null).
Edit, in case helpful
//Both of these are passed in from the prior view controller
var establishment: Establishment?
var loggedInUserID: String?
#objc func addTapped() {
// in here, we want to take the logged in user's ID and append it to the list of people that want to follow this establishment
// which is a CK Record Reference
let userID = CKRecord.ID(recordName: loggedInUserID!)
var establishmentTemp: Establishment? = establishment
var followers: [CKRecord.Reference]? = establishmentTemp?.followers
let reference = CKRecord.Reference(recordID: userID, action: CKRecord_Reference_Action.none)
if followers != nil {
if !followers!.contains(reference) {
establishmentTemp?.followers?.append(reference)
}
} else {
followers = [reference]
establishmentTemp?.followers = followers
establishment = establishmentTemp
}
[this is where the CKContainer.default.....save block pasted at the top of the question comes in]
I've looked through the various posts on 'ambiguous reference' but haven't been able to figure out the source of my issue. tried to explicitly set the types for establisthmentTemp and followers in case that was the issue (based on the solutions to other related posts) but no luck.
Afraid I'm out of ideas as a relatively inexperienced newbie!
Help appreciated.
Documenting the solution that I figured out:
Combination of two issues:
I was trying to save an updated version of a CK Record instead of updating
I was not passing a CK Record to the save() call - but a custom object
(I believe point two was the cause of the 'ambiguous reference to member'
error)
I solved it by replacing the save attempt (first block of code in the question) with:
//first get the record ID for the current establishment that is to be updated
let establishmentRecordID = establishment?.id
//then fetch the item from CK
CKContainer.default().publicCloudDatabase.fetch(withRecordID: establishmentRecordID!) { updatedRecord, error in
if let error = error {
print("error handling to come")
} else {
//then update the 'people' array with the revised one
updatedRecord!.setObject(followers as __CKRecordObjCValue?, forKey: "people")
//then save it
CKContainer.default().publicCloudDatabase.save(updatedRecord!) { savedRecord, error in
}
}
}

How to retrieve an object from a child context into another context?

I can't access a newly saved object using the objectID.
I've created a child managed object context (cmoc) from my managedContext, and created a new object inside this cmoc. Once I've inputted the variables into the new object, I have called save() on the child context, save on the parent context, and then attempted to load the object in the parent context (and in my attempts in another background context) using the objectID. This has failed and I don't know why.
I've ensured that the saving is occurring before the attempt to return the object in the new context, and that hasn't helped. I've made sure I'm saving the correct cmoc and parent context. I'm a bit at a loss.
To create the childContext, object instance of person (of type Person) and pass to the next vc (destVC):
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = coreDataStack.managedContext
let person = Person(context: childContext)
destVC.coreDataStack = coreDataStack
destVC.person = person
destVC.childContext = childContext
Then variables added to the person instance
person.cellPhone = ... [etc]
I suspect my problem is in the save code, which currently states:
guard childContext.hasChanges else {
return
}
do {
try self.newKeyNumberContext.save()
} catch let error as NSError {
[Manage error]
}
coreDataStack.saveContext()
Or alternatively in the code immediately afterwards:
let personObjectID = self.person.objectID //(does return an objectID)
let personToReturn = try? coreDataStack.managedContext.existingObject(with: personObjectID)
which doesn't return an object, returning nil instead.
Ah, turns out that this link from stackOverflow lead me to the solution. I only had a temporary ID and asking for a permanentID before saving resolved the problem. I inserted the below line above the guard childContext.hasChanges line and it sorted it out.
try? childContext.obtainPermanentIDs(for: [person])

Inserting child records is slow in coredata

I have close to 7K items stored in a relation called Verse. I have another relation called Translation that needs to load 7K related items with a single call from a JSON file.
Here is my code:
let container = getContainer()
container.performBackgroundTask() { (context) in
autoreleasepool {
for row in translations{
let t = Translation(context: context)
t.text = (row["text"]! as? String)!
t.lang = (row["lang"]! as? String)!
t.contentType = "Verse"
t.verse = VerseDao.findById(row["verse_id"] as! Int16, context: context)
// this needs to make a call to the database to retrieve the approparite Verse instance.
}
}
do {
try context.save()
} catch {
fatalError("Failure to save context: \(error)")
}
context.reset()
}
Code for the findById method.
static func findById(_ id: Int16, context: NSManagedObjectContext) -> Verse{
let fetchRequest: NSFetchRequest<Verse>
fetchRequest = Verse.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "verseId == %#", id)
fetchRequest.includesPropertyValues = false
fetchRequest.fetchLimit = 1
do {
let results =
try context.fetch(fetchRequest)
return results[0]
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
return Verse()
}
}
This works fine until I add the VerseDao.findById, which makes the whole process really slow because it has to make a request for each object to the Coredata database.
I did everything I could by limiting the number of fetched properties and using NSFetchedResultsController for data fetching but no luck.
I wonder if there's any way to insert child records in a more efficient way? Thanks.
Assuming your store type is persistent store type is sqlite (NSSQLiteStoreType):
The first thing you should check is whether you have an Core Data fetch index on the Verse objects verseId property. See this stack overflow answer for some introductory links on fetch indexes.
Without that, the fetch in your VerseDao.findById function may be scanning the whole database table every time.
To see if your index is working properly you may inspect the SQL queries generated by adding -com.apple.CoreData.SQLDebug 1 to the launch arguments in your Xcode scheme.
Other improvements:
Use NSManagedObjectContext.fetch or NSFetchRequest.execute (equivalent) instead of NSFetchedResultsController. The NSFetchedResultsController is typically used to bind results to a UI. In this case using it just adds overhead.
Don't set fetchRequest.propertiesToFetch, instead set fetchRequest.includesPropertyValues = false. This will avoid fetching the Verse object property values which you don't need to establish the relation to the Translation object.
Don't specify a sortDescriptor on the fetch request, this just complicates the query

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

How do you store a dictionary on Parse using swift?

I am very new to swift and I don't know Obj C at all so many of the resources are hard to understand. Basically I'm trying to populate the dictionary with PFUsers from my query and then set PFUser["friends"] to this dictionary. Simply put I want a friends list in my PFUser class, where each friend is a PFUser and a string.
Thanks!
var user = PFUser()
var friendsPFUser:[PFUser] = []
var friendListDict: [PFUser:String] = Dictionary()
var query = PFUser.query()
query!.findObjectsInBackgroundWithBlock {
(users: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
// The find succeeded.
println("Successfully retrieved \(users!.count) users.")
// Do something with the found objects
if let users = users as? [PFUser] {
friendsPFUser = users
for user in friendsPFUser{
friendListDict[user] = "confirmed"
}
user["friends"] = friendListDict //this line breaks things
user.saveInBackground()
}
} else {
// Log details of the failure
println("Error: \(error!) \(error!.userInfo!)")
}
}
To be clear, this code compiles but when I add
user["friends"] = friendListDict
my app crashes.
For those who might have this issues with. "NSInternalInconsistencyException" with reason "PFObject contains container item that isn't cached."
Adding Objects to a user (such as arrays or dictionaries) for security reasons on Parse, the user for such field that will be modified must be the current user.
Try signing up and using addObject inside the block and don't forget do save it!
It helped for a similar problem I had.