CKQuery from private zone returns only first 100 CKRecords from in CloudKit
Is there any limit to the result of a query to Cloudkit private default zone? I have no clue why I only receive first 100 records with the following query:
let p = NSPredicate(format: "(type == 'entered') AND (timestamp >= %@) AND (timestamp <= %@)", from, to)
let q = CKQuery(recordType: self.beaconRecordType, predicate: p)
q.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
self.privateDatabase?.performQuery(q, inZoneWithID: nil, completionHandler: { results, error in
//count = 100
println(results.count)
}
Okay. As Edwin mention in the answer, the solution is to use CKQueryOperation to fetch the initial block of data then use the "cursor" in completionBlock to fire another operation. Here is an example:
UPDATE
func fetchBeacons(from:NSDate, to:NSDate) {
let p = NSPredicate(value: true)
let q = CKQuery(recordType: self.beaconRecordType, predicate: p)
let queryOperation = CKQueryOperation(query: q)
queryOperation.recordFetchedBlock = fetchedARecord
queryOperation.queryCompletionBlock = { [weak self] (cursor : CKQueryCursor!, error : NSError!) in
if cursor != nil {
println("there is more data to fetch")
let newOperation = CKQueryOperation(cursor: cursor)
newOperation.recordFetchedBlock = self!.fetchedARecord
newOperation.queryCompletionBlock = queryOperation.queryCompletionBlock
self!.privateDatabase?.addOperation(newOperation)
}
}
privateDatabase?.addOperation(queryOperation)
}
var i = 0
func fetchedARecord (record: CKRecord!) {
println("\(NSDate().timeIntervalSinceReferenceDate*1000) \(++i)")
}
100 is the default limit for standard queries. That amount is not fixed. It can vary depending on the total iCloud load. If you want to influence that amount, then you need to use CKQueryOperation and set the resultsLimit like this: operation.resultsLimit = CKQueryOperationMaximumResults; That CKQueryOperationMaximumResults is the default and will limit it to 100 (most of the time). Don't set that value too high. If you want more records, then use the cursor of the queryCompletionBlock to continue reading more records.
I have updated GuiSoySauce's code in Swift 4.2.
func cloudKitLoadRecords(result: @escaping (_ objects: [CKRecord]?, _ error: Error?) -> Void) {
// predicate
var predicate = NSPredicate(value: true)
// query
let cloudKitQuery = CKQuery(recordType: "recordType", predicate: predicate)
// records to store
var records = [CKRecord]()
//operation basis
let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase
// recurrent operations function
var recurrentOperationsCounter = 101
func recurrentOperations(cursor: CKQueryCursor?){
let recurrentOperation = CKQueryOperation(cursor: cursor!)
recurrentOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
print("-> cloudKitLoadRecords - recurrentOperations - fetch \(recurrentOperationsCounter)")
recurrentOperationsCounter += 1
records.append(record)
}
recurrentOperation.queryCompletionBlock = { (cursor: CKQueryOperation.Cursor?, error: Error?) -> Void in
if ((error) != nil) {
print("-> cloudKitLoadRecords - recurrentOperations - error - \(String(describing: error))")
result(nil, error)
} else {
if cursor != nil {
print("-> cloudKitLoadRecords - recurrentOperations - records \(records.count) - cursor \(cursor!.description)")
recurrentOperations(cursor: cursor!)
} else {
print("-> cloudKitLoadRecords - recurrentOperations - records \(records.count) - cursor nil - done")
result(records, nil)
}
}
}
publicDatabase.add(recurrentOperation)
}
// initial operation
var initialOperationCounter = 1
let initialOperation = CKQueryOperation(query: cloudKitQuery)
initialOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
print("-> cloudKitLoadRecords - initialOperation - fetch \(initialOperationCounter)")
initialOperationCounter += 1
records.append(record)
}
initialOperation.queryCompletionBlock = { (cursor: CKQueryOperation.Cursor?, error: Error?) -> Void in
if ((error) != nil) {
print("-> cloudKitLoadRecords - initialOperation - error - \(String(describing: error))")
result(nil, error)
} else {
if cursor != nil {
print("-> cloudKitLoadRecords - initialOperation - records \(records.count) - cursor \(cursor!.description)")
recurrentOperations(cursor: cursor!)
} else {
print("-> cloudKitLoadRecords - initialOperation - records \(records.count) - cursor nil - done")
result(records, nil)
}
}
}
publicDatabase.add(initialOperation)
}
Usage
cloudKitLoadRecords() { (records, error) -> Void in
if let error = error {
// do something
} else {
if let records = records {
// do something
} else {
// do something
}
}
}
I use this code for my project to fetch all record from a record type, it's in objective c. I use "Entry" as desiredKeys.
+ (void)fetchRecordsWithType:(NSString *)recordType
completionHandler:(void (^)(NSArray *records, NSError *error))completionHandler {
NSPredicate *truePredicate = [NSPredicate predicateWithValue:YES];
CKQuery *query = [[CKQuery alloc] initWithRecordType:recordType
predicate:truePredicate];
CKQueryOperation *queryOperation = [[CKQueryOperation alloc] initWithQuery:query];
queryOperation.desiredKeys = @[@"Entry"];
NSMutableArray *results = [NSMutableArray new];
queryOperation.recordFetchedBlock = ^(CKRecord *record) {
[results addObject:record]; };
queryOperation.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error) {
[self retrieveNextBatchOfQueryFromCursor:cursor
results:results
error:error
completionHandler:completionHandler]; };
[[self CloudKitContainer].privateCloudDatabase addOperation:queryOperation]; }
+ (void)retrieveNextBatchOfQueryFromCursor:(CKQueryCursor *)cursor
results:(NSMutableArray *)results
error:(NSError *)error
completionHandler:(void (^)(NSArray *records, NSError *error))completionHandler {
// CloudKit apparently has query limit
if (cursor != nil
&& !error) {
CKQueryOperation *nextOperation = [[CKQueryOperation alloc] initWithCursor:cursor];
nextOperation.recordFetchedBlock = ^(CKRecord *record) {
[results addObject:record]; };
nextOperation.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error) {
[self retrieveNextBatchOfQueryFromCursor:cursor
results:results
error:error
completionHandler:completionHandler]; };
[[self CloudKitContainer].privateCloudDatabase addOperation:nextOperation]; }
else {
dispatch_async(dispatch_get_main_queue(), ^(void){
completionHandler(results, error); }); }}
Another way of running it inside a function with a completion handler that won't stop until all records are fetched. This can be re-used by different different view controllers across the app.
Query
func cloudKitLoadRecords(result: (objects: [CKRecord]?, error: NSError?) -> Void){
// predicate
var predicate = NSPredicate(value: true)
// query
let cloudKitQuery = CKQuery(recordType: "ClassName", predicate: predicate)
// records to store
var records = [CKRecord]()
//operation basis
let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase
// recurrent operations function
var recurrentOperationsCounter = 101
func recurrentOperations(cursor: CKQueryCursor?){
let recurrentOperation = CKQueryOperation(cursor: cursor!)
recurrentOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
print("-> cloudKitLoadRecords - recurrentOperations - fetch \(recurrentOperationsCounter++)")
records.append(record)
}
recurrentOperation.queryCompletionBlock = { (cursor:CKQueryCursor?, error:NSError?) -> Void in
if ((error) != nil)
{
print("-> cloudKitLoadRecords - recurrentOperations - error - \(error)")
result(objects: nil, error: error)
}
else
{
if cursor != nil
{
print("-> cloudKitLoadRecords - recurrentOperations - records \(records.count) - cursor \(cursor!.description)")
recurrentOperations(cursor!)
}
else
{
print("-> cloudKitLoadRecords - recurrentOperations - records \(records.count) - cursor nil - done")
result(objects: records, error: nil)
}
}
}
publicDatabase.addOperation(recurrentOperation)
}
// initial operation
var initialOperationCounter = 1
let initialOperation = CKQueryOperation(query: cloudKitQuery)
initialOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
print("-> cloudKitLoadRecords - initialOperation - fetch \(initialOperationCounter++)")
records.append(record)
}
initialOperation.queryCompletionBlock = { (cursor:CKQueryCursor?, error:NSError?) -> Void in
if ((error) != nil)
{
print("-> cloudKitLoadRecords - initialOperation - error - \(error)")
result(objects: nil, error: error)
}
else
{
if cursor != nil
{
print("-> cloudKitLoadRecords - initialOperation - records \(records.count) - cursor \(cursor!.description)")
recurrentOperations(cursor!)
}
else
{
print("-> cloudKitLoadRecords - initialOperation - records \(records.count) - cursor nil - done")
result(objects: records, error: nil)
}
}
}
publicDatabase.addOperation(initialOperation)
}
Usage
cloudKitLoadRecords() { (queryObjects, error) -> Void in
dispatch_async(dispatch_get_main_queue()) {
if error != nil
{
// handle error
}
else
{
// clean objects array if you need to
self.objects.removeAll()
if queryObjects!.count == 0
{
// do nothing
}
else
{
// attach found objects to your object array
self.objects = queryObjects!
}
}
}
}