Core Data Inheritance entities not being saved - swift

I have the following Data Model in Core Data:
*Note: I Setup Attachment to be Abstract and ExpenseAttachment and LeaveAttachment have their parent set as Attachment
When I try to save ExpenseAttachment or LeaveAttachment, I run intro the problem that the code to save a new attachment runs fine with no errors, but nothing really happens, since when I try to retrieve it there are no records.
So I use this code so save:
func syncExpenseAttachments(attachmentResponse: AttachmentResponse, expenseID: Int64)
{
let backgroundContext : NSManagedObjectContext! = DataController.sharedInstance().backgroundContext
backgroundContext.perform
{
let attachmentExpense = try? backgroundContext.fetch(Expense.fetchRequest()).filter { $0.tkID == expenseID }
let attachment = ExpenseAttachment(context: backgroundContext)
attachment.tkID = Int64(attachmentResponse.id ?? 0)
attachment.createdDate = attachmentResponse.createdDate ?? Date()
attachment.name = attachmentResponse.name ?? ""
attachment.flaggedForDelete = false
attachment.data = attachmentResponse.data as Data? ?? nil
if let attachmentExpense = attachmentExpense {
attachment.expense = attachmentExpense[0]
}
try? backgroundContext.save()
}
}
And this code to get the records:
let attachmentRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ExpenseAttachment")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: false)
attachmentRequest.sortDescriptors = [sortDescriptor]
do {
let coreDataExpenseAttachments = try DataController.sharedInstance().viewContext.fetch(attachmentRequest) as! [ExpenseAttachment]
//Do some work
} catch {
print(error.localizedDescription)
}
After the save, trying to retrieve attachments returns nothing. However if I simply use the Attachment entity with saving, I can actually see records when I also try to retrieve data for the attachments enitity. I though you do not create or save the Abstract Parent Entity, just the Child Entities?

Related

CoreData Stops the saving after Heavyweight migration Xcode 14

Due to the enhancement in my project, I have done the heavyweight migration, now after heavy weight migration, CoreData is unable to store data it always return empty.
I have a class Function
class func insert(object: [String: Any], entityName: String) -> NSManagedObject? {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: entityName, in: context)!
let managedObject = NSManagedObject(entity: entity, insertInto: context)
let keys = Array(entity.attributesByName.keys)
print("Keys:\(keys)\nobject:\(object)")
for (key, element) in object {
if keys.contains(key) {
managedObject.setValue(element, forKey: key)
}
}
do {
print(managedObject.isUpdated)
try context.save()
print(managedObject.isUpdated)
print("Success insert")
} catch {
print("Error in Insert: \(error)")
}
return managedObject
}
so I pass my Object with the key value pair in form of Dictionary, I pass the entityName to this function so it never return nil in managedObject While it always show false on print(managedObject.isUpdated) on both lines. Here is my console logs for saving data.
Keys:["contactIdentifier", "timeAndDate", "receiverId", "createdAt", "updatedAt", "i-----------g", "se---d", "duration", "senderId", "number", "name", "isFromiOS", "opponentsIds", "m-------d"]
object:["sessionID": "E1ECAB27-D312", "message": "--------", "contactIdentifier": "------- ", "I----p": "1", "contactNumbers": "------------,-----------", "V-----l": "1", "co------pe": "2", "createdAt": 2022-10-27 11:58:18 +0000, "opponentsIds": "---, ----"]
false
false
Success insert
updateCallInformation:<CallHistory2: 0x2835e99f0> (entity: CallHistory2; id: 0xa46b7d533bd5d14b <x-coredata://BAF5AFD6-8567-4485-8938-E13D3BD7B314/CallHistory2/p1>; data: {
contactIdentifier = "MDanishPK ";
createdAt = "2022-10-27 11:58:18 +0000";
duration = nil;
incomingOutgoing = nil;
isFromiOS = 0;
missedReceived = nil;
name = nil;
number = nil;
opponentsIds = nil;
receiverId = nil;
senderId = nil;
sessionID = "E1ECAB27-D312-4402-91D3-87AEBAC1A709";
timeAndDate = nil;
updatedAt = nil;
})
In console ---- I added intentially here for the privacy. There are some proper values and keys here.
If I revert my migration and goto the older branch using git than its working fine and save data properly. But I need heavy weight migration which I achieved using this article Kindly help me out and guide me what I have to do To save managedObject? and what I am doing wrong in saving managedObject? I changed all the table name but still its not saving data. On every launch I also lost the data.
Update Question
After Insert data I am using this fetch method, It always return me 0 against the array of [NSManagedObject]?
class func fetchList(entityName: String, predicate: NSPredicate? = nil, sortBy: [(key: String, ascending: Bool)]? = nil) ->
[NSManagedObject]? {
let context = CoreDataStack.context
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
fetchRequest.predicate = predicate
if let sortedBy = sortBy {
var sortDescriptors = [NSSortDescriptor]()
for sortElement in sortedBy {
let sortDescriptor = NSSortDescriptor(key: sortElement.key, ascending: sortElement.ascending)
sortDescriptors.append(sortDescriptor)
}
fetchRequest.sortDescriptors = sortDescriptors
}
do {
let results = try context.fetch(fetchRequest)
return results
} catch {
print("Could not fetch: \(error)")
}
return nil
}

NSPersistentStoreRemoteChange doesn't call

I work on Core Data sync with iCloud. It works great, but Core Data updates changes only after leave app. I was learning about it and I found this documentation: https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes. I've decide to use:
NotificationCenter.default.addObserver(self, selector: #selector(storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator) In storeRemoteChange(_:)) I've put print command to check when Notification fire. Notification doesn't fire, sometimes after 5 minutes, but changes saved on iCloud are immediately.
My code from persistentContainer initialize contains
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
privateStoreDescription.setOption(true as NSNumber,
forKey: remoteChangeKey)
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
NotificationCenter.default.addObserver(self, selector: #selector(storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
Please help me. Thank you in advance
Ok, how the data is handled thru its lifecycle at your project I don't know, but I will try with some general input.
To find the correct way to chose what data to keep if local data and cloud data is different is not a right or wrong answer to me at least.
We ca assume cloud is always correct you can mirror
Or you can when you set a Core data Entity
extension myEntity: Comparable {
static func setEntity(_ id: String, info: EntityInfo, in context: NSManagedObjectContext) -> MyEntity {
let request = fetchRequest(NSPredicate(format: "id_ = %#", string))
let results = (try? context.fetch(request)) ?? []
if let entity = results.first {
return entity
} else {
let entity = Entity(context: context)
entity.id = id
entity.name = info.name
try context.save()
return airline
}
}
static func fetchRequest(_ predicate: NSPredicate) -> NSFetchRequest<MyEtity> {
let request = NSFetchRequest<Airline>(entityName: "MyEntity")
request.sortDescriptors = [NSSortDescriptor(key: "name_", ascending: true)]
request.predicate = predicate
return entity
}
struct EntityInfo {
let name: String
}
}
Or when a context updates, maybe you have two context, and only update the one, and need to call the other for your data to run as you want in your notification block
var savedOK = false
managedObjectContext.performAndWait() {
// Perform operations with the context.
do {
try managedObjectContext.save()
savedOK = true
} catch {
print("Error saving context: \(error)")
}
}

how to get multiple record types linked via CKreference from cloud kit

I create a series of CK references with a contact having mutiple locations, each locaiton ahving a provider and each provider having a meter and so forth
let self.currentLocationCkRecord["ownningContact"] = CKReference(record: self.currentContactCkRecord!, action: CKReferenceAction.deleteSelf)
let self.currentProviderCkRecord["ownningLocation"] = CKReference(record: self.currentLocationCkRecord!, action: CKReferenceAction.deleteSelf)
let self.currentMeterCkRecord["ownningProvider"] = CKReference(record: self.currentProviderCkRecord!, action: CKReferenceAction.deleteSelf)
when I retrieve all the records including referenced records I run into an issue if nesting code to get each of the referenced records
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "Contact", predicate: predicate)
privateDB?.perform(query, inZoneWith: self.ckRecordZoneID) { (records, error) in
// handle error
if let records = records {
for aRecord in records {
// I process location CKRecord
self.alliCloudLocations.append(record)
let contactID = aRecord.recordID
let recordToMatch = CKReference(recordID: contactID, action: .deleteSelf)
let predicate = NSPredicate(format: "owningContact == %#", recordToMatch)
// Create the query object.
let query = CKQuery(recordType: Cloud.Entity.Location, predicate: predicate)
let ckQueryOpLocation = CKQueryOperation(query: query)
ckQueryOpLocation.queryCompletionBlock = { (cursor, error) in
print("Query completion block called")
guard error == nil else {
if let ckerror = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckerror)
}
return
}
if cursor != nil {
let nextQueryOp = CKQueryOperation(cursor: cursor!)
nextQueryOp.recordFetchedBlock = = { (record: CKRecord) in
self.alliCloudLocations.append(record)
print(record)
// TO DO: I need to get a provider CKRecord and for each location CKRecord and for each provider CKRecord I ned to get a meter CKRecord
}
nextQueryOp.ZoneID = self.CKRecordZoneID
nextQueryOp.queryCompletionBlock = ckQueryOpLocation.queryCompletionBlock
nextQueryOp.desiredKeys = = ["locationName"]
nextQueryOp.desiredKeys = = ["zip"]
nextQueryOp.desiredKeys = = ["locationType"]
nextQueryOp.resultsLimit = ckQueryOpLocation.resultsLimit
//important
ckQueryOpLocation = nextQueryOp
privateDB.add(ckQueryOpLocation)
print("added next fetch")
}
}
}
}
// Add the CKQueryOperation to a queue to execute it and process the results asynchronously.
privateDB?.add(ckQueryOpLocation)
}
In the above code for each Contact CKRecord, I am fetching location CKRecords and then as you can see from my above // TO DO comment statement: I need to call the entire perform CKQuery and QueryCompletionBlock for each of the referenced records: provider, and meter
My question is when I pull the location CKRecord does it pull all the referenced Provider CKRecord and Meter CKRecord; if so how to retrieve each of them
or
Do I have to fetch each of the Provider and Meters CKRecords individually and if so code gets very complicated inside the recordFetchedBlock method since that is where I have to call the nested code.
Can anyone advise how to structure this code in a simple and easy to operate manner?

How to set an entities attribute via relationship?

I have the following entities and relationship
I want to be able to set an exercise to have a nil result for its routine name relationship, if that makes sense? so that it can later be set as a routine name when the routine entity is formed.
My question is, how do you set this sort of attribute up? I am trying the following code but it causes a fatal crash:
userExercise.usersroutine?.name = nil
My logic being that i take the exercise and follow the relationship to the name property and set it to nil?
Thanks for any correction and clarification on my logic
EDIT: Added my existing exercise and routine save functions
func createExercise() {
guard let managedObjectContext = managedObjectContext else { return }
if let userExercise = userExercise {
userExercise.name = userExerciseName.text
userExercise.sets = Int64(userSetsCount)
userExercise.reps = Int64(userRepsCount)
userExercise.weight = Double(self.userExerciseWeight.text!)!
userExercise.id = UUID().uuidString
userExercise.routine = nil
}
do {
try managedObjectContext.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
Routine Creation:
func createRoutine() {
guard let managedObjectContext = managedObjectContext else { return }
let userRoutine = UserRoutine(context: managedObjectContext)
userRoutine.name = workoutNameTextfield.text
do {
try managedObjectContext.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
Current Fetch Request:
fileprivate lazy var fetchedResultsController: NSFetchedResultsController<UserExercise> = {
let fetchRequest: NSFetchRequest<UserExercise> = UserExercise.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
Please check the implementation below I'have created some exercises and routines. Also read comments in code, this will help you figure out how to go about it.
Function to add a new exercise
func createExercise(weight: Int16, respetitions: Int16, name: String, routine: Routine?)->Exercise? {
let context = getMainContext()
let exercise = NSEntityDescription.insertNewObject(forEntityName: "Exercise", into: context) as! Exercise
exercise.setValue(weight, forKey: "weight")
exercise.setValue(name, forKey: "name")
exercise.setValue(respetitions, forKey: "rep")
do {
try context.save()
return exercise
}
catch
{
fatalError("unable to Ssavve")
}
}
Function to add a new routine
func createRoutine(name: String, exercises:[Exercise]) {
let context = getMainContext()
let routine = NSEntityDescription.insertNewObject(forEntityName: "Routine", into: context) as! Routine
routine.name = name
//Iterate over Exercise objects & check if routine is nil.
//Here if routine is not nil it menas your exercise is already assigned to a routine.
//If routine is nil assign routine.addToRelationship(<#T##value: Exercise##Exercise#>) and Also assign routine to the execise.
do {
try context.save()
}
catch
{
fatalError("unable to Ssavve")
}
}
Function to get main NSManagedObjectContext on which we can perform core-data actions
func getMainContext() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
Below, First I create few exercises without any relationship to routines
"The routine doesnt exist when the exercises are created, it is created afterward and its name is set"
and then create routines by passing some exercises (You can refer to other answer on how to fetch exercises with routine as nil values)
func initializer() {
//I'm adding exercises first without routines
let ex1 = self.createExercise(weight: 10, respetitions: 4, name: "Exercise1", routine: nil)
let ex2 = self.createExercise(weight: 5, respetitions: 10, name: "Exercise2", routine: nil)
let ex3 = self.createExercise(weight: 20, respetitions: 2, name: "Exercise3", routine: nil)
let ex4 = self.createExercise(weight: 5, respetitions: 10, name: "Exercise2", routine: nil)
self.createRoutine(name: "Routine 1", exercises: [ex1!, ex2!]) //You can pass all the exercises or use fetch request to query exercises with routine as nil
self.createRoutine(name: "Routine 2", exercises: [ex3!, ex4!])
self.createRoutine(name: "Routine 3", exercises: [ex1!, ex2!]) //This routine shall not be adding any execises as they are already added to othe routines
}
Updating create routine Function to query results of UserExercise which has usersroutine as nil
func createRoutine() {
guard let managedObjectContext = managedObjectContext else { return }
let userRoutine = UserRoutine(context: managedObjectContext)
userRoutine.name = workoutNameTextfield.text
//Getting nil value User Exercises
let request: NSFetchRequest<UserExercise> = UserExercise.fetchRequest()
request.predicate = NSPredicate(format: "usersroutine == nil")
do {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let queryResults = try context.fetch(request)
//I like to check the size of the returned results!
print ("num of results = \(queryResults.count)")
//You need to convert to NSManagedObject to use 'for' loops
for exercise in queryResults as [NSManagedObject] {
//get the Key Value pairs (although there may be a better way to do that...
print("Exercise NAME: \(exercise.value(forKey: "name"))")
}
} catch {
print("Error with request: \(error)")
}
do {
try managedObjectContext.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
It doesn't seem that you should need to use the name attribute at all. This attribute should be used for storing the actual name of the UserRoutine and not for anything relationship based.
The relationships between entities in Core Data don't rely on a particular attribute of an entity, but between the entities themselves.
"I want the routine builder to look at the exercises and import all the exercises with nil in the relationship into it"
So...
Create a fetch request to fetch all the entities of UserExercise that don't have a related UserRoutine (i.e. where userroutine is nil).
let orphanedExerciseFetchRequest = NSFetchRequest(entityName: "UserExercises")
orphanedExerciseFetchRequest.predicate = NSPredicate(format: "userroutine == nil)
Execute this fetch request to get an array of UserExercises (with no related routine)
let orphanedExercises = managedObjectContext.executeFetchRequest(orphanedExerciseFetchRequest())
"creating a routine with attributed exercises"
Set the fetched UserExercise entitiy's property userRoutine to your routine (and don't forget to save the changes in your managed object context).
myRoutine.userexercises = orphanedExercises
Later, if you want to get the exercises for a particular routine:
let fetchRequest = NSFetchRequest(entityName: "UserExercises")
fetchRequest.predicate = NSPredicate(format: "userroutine == %#", someUserRoutine)

CloudKit Query Operation only returns 300 results

I am currently setting up CloudKit as a replacement to Parse and need to download all of my user records. I currently have around 600 records but I am only receiving 300.
I'm using a custom record zone called "User" rather than the default "Users" record zone as this app will only ever be tied to one appID.
The code I am using is based on the answer to the below question but it's not working for me. It seems that the query operation does not run when the cursor is nil as the print(userArray) is never called. Thanks in advance for your help!
CKQuery from private zone returns only first 100 CKRecords from in CloudKit
func queryAllUsers() {
let database = CKContainer.defaultContainer().privateCloudDatabase
let query = CKQuery(recordType: "User", predicate: NSPredicate(value: true))
let queryOperation = CKQueryOperation(query: query)
queryOperation.recordFetchedBlock = self.createUserObject
queryOperation.queryCompletionBlock = { cursor, error in
if cursor != nil {
print("there is more data to fetch")
let newOperation = CKQueryOperation(cursor: cursor!)
newOperation.recordFetchedBlock = self.createUserObject
newOperation.queryCompletionBlock = queryOperation.queryCompletionBlock
database.addOperation(newOperation)
} else {
print(userArray) //Never runs
}
}
database.addOperation(queryOperation)
}
func createUserObject(record: CKRecord) {
let name = record.objectForKey("Name") as! String!
let company = record.objectForKey("Company") as! String!
let dateInductionCompleted = record.objectForKey("DateInductionCompleted") as! NSDate!
var image = UIImage()
let imageAsset = record.objectForKey("Image") as! CKAsset!
if let url = imageAsset.fileURL as NSURL? {
let imageData = NSData(contentsOfURL:url)
let mainQueue = NSOperationQueue.mainQueue()
mainQueue.addOperationWithBlock() {
image = UIImage(data: imageData!)!
userArray.append(User(name: name, company: company, image: image, dateInductionCompleted: dateInductionCompleted))
}
}
print(userArray.count)
}
UPDATE
The question has been answered, it was possibly an inherent bug when using a cursor for large queries. The code now works by using a recursive function, working code below:
func queryRecords() {
let database = CKContainer.defaultContainer().privateCloudDatabase
let query = CKQuery(recordType: "User", predicate: NSPredicate(value: true))
let queryOperation = CKQueryOperation(query: query)
queryOperation.qualityOfService = .UserInitiated
queryOperation.recordFetchedBlock = populateUserArray
queryOperation.queryCompletionBlock = { cursor, error in
if cursor != nil {
print("There is more data to fetch")
self.fetchRecords(cursor!)
}
}
database.addOperation(queryOperation)
}
func fetchRecords(cursor: CKQueryCursor?) {
let database = CKContainer.defaultContainer().privateCloudDatabase
let queryOperation = CKQueryOperation(cursor: cursor!)
queryOperation.qualityOfService = .UserInitiated
queryOperation.recordFetchedBlock = populateUserArray
queryOperation.queryCompletionBlock = { cursor, error in
if cursor != nil {
print("More data to fetch")
self.fetchRecords(cursor!)
} else {
print(userArray)
}
}
database.addOperation(queryOperation)
}
func populateUserArray(record: CKRecord) {
let name = record.objectForKey("Name") as! String!
let company = record.objectForKey("Company") as! String!
let dateInductionCompleted = record.objectForKey("DateInductionCompleted") as! NSDate!
var image = UIImage()
let imageAsset = record.objectForKey("Image") as! CKAsset!
if let url = imageAsset.fileURL as NSURL? {
let imageData = NSData(contentsOfURL:url)
let mainQueue = NSOperationQueue.mainQueue()
mainQueue.addOperationWithBlock() {
image = UIImage(data: imageData!)!
userArray.append(User(name: name, company: company, image: image, dateInductionCompleted: dateInductionCompleted))
}
}
print(userArray.count)
}
Could you try setting:
queryOperation.qualityOfService = .UserInitiated
This will indicate that your user interaction requires the data.
Otherwise it could happen that de request is ignored completely.
As discussed below the actual answer was that you should not re-use completion blocks. Instead you should create a recursive function for fetching the next records from a cursor. A sample of that can be found at: EVCloudKitDao