Saving & Fetching CloudKit References - swift

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))

Related

Pass the information from CKRecord to user defined class type

I have to following class type
class Recipe {
private let id: CKRecord.ID
let name: String
let coverPhoto: CKAsset?
init?(record: CKRecord, database: CKDatabase) {
guard
let name = record["name"] as? String
else { return nil }
id = record.recordID
self.name = name
coverPhoto = record["coverPhoto"] as? CKAsset
}
and in my viewcontroller, I want to display list of names that I fetched from cloudkit. So, i am trying to find a way to convert CKRecord to my class type Recipe.
I tried this but failed
let query = CKQuery(recordType: "GroceryItem", predicate: NSPredicate(value:true))
database.perform(query, inZoneWith: nil) { [weak self] records, error in
guard let records = records,error == nil else {
return
}
DispatchQueue.main.async {
self?.items = records.compactMap({ $0.value as? Recipe }) // gives error ambigous value
self?.tableView.reloadData()
}
}
Records is the fetched variable from cloudkit as CKRecord
items is array of Recipe, [Recipe]
I want to transfer the data from records to items.
You cannot cast the type. You have to call init.
First remove the database parameter from the init method, it’s obviously not needed
init?(record: CKRecord) { …
Then replace
self?.items = records.compactMap({ $0.value as? Recipe })
with
self?.items = records.compactMap(Recipe.init)

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?

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)

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

How can I speed up performQuery in Swift?

So I am creating this app that uses CloudKit to save and fetch images and text from the Cloud. The problem is that I can only access the results after the whole fetch is done. I would like to be able to fetch each record individually as it is fetched. Here is the code.
func fetchPost() {
spinner.startAnimating()
if imageView.image != nil {
spinner.alpha = 0
}
var imageData = [UIImage]()
var text = [String]()
let predicate = NSPredicate(value: true)
let sort = NSSortDescriptor(key: "creationDate", ascending: false)
let query = CKQuery(recordType: "Post",
predicate: predicate)
query.sortDescriptors = [sort]
publicDB.performQuery(query, inZoneWithID: nil) {
results, error in
if error != nil {
dispatch_async(dispatch_get_main_queue()) {
println("Query failed")
return
}
} else {
println("test")
var number = 0
for record in results {
if let pictureRecord = record as? CKRecord {
let post = Post(record: pictureRecord, database: self.publicDB)
let postImageData = post.imageData
let postText = post.text
self.images.append(UIImage(data: postImageData)!)
self.texts.append(postText)
println("\"\(postText)\" is the text. Fetch successful.")
if number == 0 {
self.imageView.image = self.images[0]
self.nameLabel.text = self.texts[0]
}
++number
} else {
println("Records failed")
}
}
}
self.spinner.stopAnimating()
self.spinner.alpha = 1
}
}
Thanks!
If you use CKQueryOperation you can set a callback (recordFetchedBlock) that will be called for each record as it is fetched from the server.