CloudKit Query Operation only returns 300 results - swift

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

Related

How to fetch Firestore document that contains a reference to another document in Swift 5?

I am trying to fetch data from a Firebase Database in Swift 5.
I have two collections: "users" and "locations" (locations has a reference to user).
I have attached a snapshot listener to the locations collection so I will fetch data every time. there is a change and add it to a global array.
Therefore after I fetch the location document I want to also fetch the user that is referencing to.
I had to add a DispatchGroup so it will wait for all users to get fetched and added to the array before passing the array through the completion block.
However, it is adding duplicates of each location into an array. How should I fix this problem? Thanks in advance for your help :).
static func startListener(completion: #escaping ((_ data: [Location]) -> Void)){
db.collection(LOCATIONS_DOCUMENT).addSnapshotListener {
(snap,error) in
let dispatchGroup = DispatchGroup()
if(error != nil){
print(error!)
}
else{
self.locations.removeAll()
for location in snap!.documents{
dispatchGroup.enter()
let map = location.data()
let id = location.documentID
let image = map["image"] as! String
let title = map["title"] as! String
let latitude = map["latitude"] as! NSNumber
let longitude = map["longitude"] as! NSNumber
let userRef = map["user"] as! DocumentReference
let user = User();
userRef.getDocument { (document, error) in
if(error == nil){
let userMap = document!.data()
let userId = document!.documentID
let userFirstName = userMap!["firstName"] as! String
user.id = userId
user.firstName = userFirstName
}
let newLocation = Location(id: id, image: image, title: title, latitude: Double(truncating: latitude), longitude: Double(truncating: longitude), user: user)
self.locations.append(newLocation)
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
completion(locations)
}
}
}

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

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 not working with Cellular (Time exceeded)

I'm using following code to fetch Data from iCloud:
func fetchShoppingList() {
let container = CKContainer.default()
let publicDB = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "ShoppingList", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.allowsCellularAccess = true
operation.qualityOfService = .userInitiated
publicDB.add(operation)
publicDB.perform(query, inZoneWith: nil) { [unowned self] results, error in
if error != nil {
print(error)
}
else {
for var value in results! {
let shoppingListEntry = ShoppingListEntry()
shoppingListEntry.index = value.value(forKey: "index") as! Int
shoppingListEntry.product = value.value(forKey: "product") as! String
shoppingListEntry.amount = value.value(forKey: "amount") as! Int
shoppingListEntry.priority = value.value(forKey: "priority") as! Int
if value.value(forKey: "isSelected") as! String == "true" {
shoppingListEntry.isSelected = true
}
else {
shoppingListEntry.isSelected = false
}
self.shoppingListEntrys.append(shoppingListEntry)
}
OperationQueue.main.addOperation({ () -> Void in
self.tableViewShoppingList.reloadData()
})
}
}
}
Everything works fine if my phone is connected via Wifi, but if I'm using Cellular I get following error: CKError 0x170244e30: "Network Failure" (4/-1001); "Zeitüberschreitung bei der Anforderung." So there seems to be a problem with time exceeding. I looked for a solution and found a post, saying I have to add the operation lines but nothing changed.
Can anybody help me please?

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.