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

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?

Related

Saving & Fetching CloudKit References

I'm having trouble creating with CloudKit References. Data is being saved into CloudKit but its not referencing its parent (list). Don't know what i'm doing wrong, any help would be much appreciated!
Saving Method
var list: CKRecord?
var item: CKRecord?
#objc func save() {
let name = nameTextField.text! as NSString
//Fetch Private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
if item == nil {
//Create Record
item = CKRecord(recordType: RecordTypeItems)
//Initialization Reference
guard let recordID = list?.recordID else { return }
let listReference = CKRecord.Reference(recordID: recordID, action: .deleteSelf)
item?.setObject(listReference, forKey: "list")
}
item?.setObject(name, forKey: "name")
//Save Record
privateDatabase.save(item!) { (record, error) in
DispatchQueue.main.sync {
self.processResponse(record: record, error: error)
}
}
}
Fetch Method
var list: CKRecord!
var items = [CKRecord]()
private func fetchItems() {
//Fetch Private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
//Initialize Query
guard let recordID = list?.recordID else { return }
let reference = CKRecord.Reference(recordID: recordID, action: .deleteSelf)
let query = CKQuery(recordType: RecordTypeItems, predicate: NSPredicate(format: "list == %#", [reference]))
//Configure Query
query.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
//Peform Query
privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
DispatchQueue.main.sync {
self.processResponseForQuery(records: records, error: error)
}
}
}
Where you are creating your query to retrieve items referencing the list, should the list reference in the predicate format string be inside an array? If you create the item's reference like item?.setObject(listReference, forKey: "list"), CloudKit will infer the list field to be a single CKRecord.Reference, so the query would be:
let query = CKQuery(recordType: RecordTypeItems, predicate: NSPredicate(format: "list == %#", reference))

Core Data Inheritance entities not being saved

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?

CloudKit how to modify existing record (swift 3)

How can I modify an existing CloudKit record?
I receive a record from CloudKit with this code:
let name = tmpVar as! String
let container = CKContainer.default()
let privateDatabase = container.privateCloudDatabase
var predicate = NSPredicate(format: "email == %#", name)
var query = CKQuery(recordType: "MainTable", predicate: predicate)
privateDatabase.perform(query, inZoneWith: nil) { (results, error) -> Void in
if error != nil {
pass
}
else {
if (results?.count)! > 0 {
for result in results! {
self.likedArr.append(result)
}
if let checker = self.likedArr[0].object(forKey: "like") as? String {
print ("CHEKER IS \(checker)")
let intChecker = Int(checker)
let result = intChecker! + 1
} else {
print ("EMPTY")
}
} else {
print ("Login is incorrect")
}
OperationQueue.main.addOperation({ () -> Void in
// self.tableView.reloadData()
// self.tableView.isHidden = false
// MBProgressHUD.hide(for: self.view, animated: true)})}
and how to return it back modified value of "like" key to the owner "name"?
When you get the records from the cloud, you can cast them to CKRecords.
In this CKRecord object you just modify the values you want to update, and then save it all again to the cloud. The CKRecordId must be the same, otherwise you'll just make a new record.
here is how to modify the records:
MyCKRecord.setValue(object, forKey: "myKey")
When you call the query, you get an array of CKRecord objects. Use the subscript to edit the record:
record["key"] = value as CKRecordValue
when you're finished, take the CKRecord and use either CKModifyRecordsOperation or CKDatabase.save(_:completionHandler:) to save it back to the server.
Sharing my solution:
self.likedArr[0].setValue(1, forKey: "like")
let saveOper = CKModifyRecordsOperation()
saveOper.recordsToSave = self.likedArr
saveOper.savePolicy = .ifServerRecordUnchanged
saveOper.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
if saveOper.isFinished == true {
}
}
privateDatabase.add(saveOper)

Swift CloudKit QueryOperation Duplicates

When I perform a query operation, despite only having 501 records in the cloudkit dashboard, I get around 1542 results (all duplicates).
This is my code:
func queryForTable() -> Void {
self.arrayOfFoodItems.removeAllObjects()
let container = CKContainer.defaultContainer()
let resultPredicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "FoodItems", predicate: resultPredicate)
let queryOp = CKQueryOperation(query: query)
let operationQueue = NSOperationQueue()
executeQueryOperation(queryOp, onOperationQueue: operationQueue)
}
func executeQueryOperation(queryOperation: CKQueryOperation, onOperationQueue operationQueue: NSOperationQueue){
queryOperation.database = CKContainer.defaultContainer().publicCloudDatabase
queryOperation.recordFetchedBlock = self.addRecordToArray
queryOperation.queryCompletionBlock = { (cursor: CKQueryCursor?, error: NSError?) -> Void in
if cursor != nil {
if let queryCursor = cursor{
let queryCursorOperation = CKQueryOperation(cursor: queryCursor)
self.executeQueryOperation(queryCursorOperation, onOperationQueue: operationQueue)
}
}
else {
self.sortToSectionsAndReloadData()
}
}
operationQueue.addOperation(queryOperation)
}
How do I solve this problem? Thanks a lot!
UPDATE: Here's the other 2 functions I'm using. As stated in the comments, I'm calling queryForTable() in viewDidLoad.
func sortToSectionsAndReloadData() {
for (var i = 0; i < self.arrayOfSections.count; i++) {
self.arrayOfArrays[i].removeAllObjects()
let prefix:String = self.arrayOfSections[i]
let array:NSMutableArray = self.arrayOfArrays[i] as! NSMutableArray
for object in self.arrayOfFoodItems {
let name = object["itemName"] as! String
if name.lowercaseString.hasPrefix(prefix.lowercaseString) {
array.addObject(object)
}
}
}
NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
self.tableView.reloadData()
}
}
func addRecordToArray (record: CKRecord!) {
self.arrayOfFoodItems.addObject(record)
let recordItemName = record["itemName"]
print("\(recordItemName)")
}
With each queryCompletionBlock you will receive all record that are fetched up to the cursor. So the first query you will get a result of about 100 records, then 200, then 300, then 400 and then 500. In your case you add those to your results each time. If you add these up, then you end up with 1500 records. So instead of adding the results to your data array you should replace the data array with the results.
I would venture that the queryForTable() function is getting called again before the previous call has completed its query operation. You would get parallel queries feeding your array, which gets reinitialized by the last queryForTable() but still receives data from the ongoing queries that have not yet finished receiving data.

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