Swift Cloudkit and Ckquery: How to Iteratively Retrieve Records When Queryresultblock Returns a Query Cursor

Swift CloudKit and CKQuery: how to iteratively retrieve records when queryResultBlock returns a query cursor

No need for queryResultBlock in Swift 5.5.

I use this because my CKRecord types are always named the same as their Swift counterparts. You can replace recordType: "\(Record.self)" with your recordType if you want, instead.

public extension CKDatabase {
/// Request `CKRecord`s that correspond to a Swift type.
///
/// - Parameters:
/// - recordType: Its name has to be the same in your code, and in CloudKit.
/// - predicate: for the `CKQuery`
func records<Record>(
type _: Record.Type,
zoneID: CKRecordZone.ID? = nil,
predicate: NSPredicate = .init(value: true)
) async throws -> [CKRecord] {
try await withThrowingTaskGroup(of: [CKRecord].self) { group in
func process(
_ records: (
matchResults: [(CKRecord.ID, Result<CKRecord, Error>)],
queryCursor: CKQueryOperation.Cursor?
)
) async throws {
group.addTask {
try records.matchResults.map { try $1.get() }
}

if let cursor = records.queryCursor {
try await process(self.records(continuingMatchFrom: cursor))
}
}

try await process(
records(
matching: .init(
recordType: "\(Record.self)",
predicate: predicate
),
inZoneWith: zoneID
)
)

return try await group.reduce(into: [], +=)
}
}
}

How do I use CloudKit to query iteratively with CKQueryOperation until cursor is nil?

One option that should work is to reference the queryCompletionBlock of the original query operation.

let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: DatabaseNameStrings.recordTypeEntry, predicate: predicate)
let queryOperation = CKQueryOperation(query: query)
queryOperation.desiredKeys = [DatabaseNameStrings.fieldNameCreatedAt, DatabaseNameStrings.fieldNameText]
let queryCompletionBlock = {
(cursor: CKQueryOperation.Cursor?, error: Error?) in
if let error = error {
print(error.localizedDescription)
} else if let cursor = cursor {
let newOperation = CKQueryOperation(cursor: cursor)
newOperation.desiredKeys = queryOperation.desiredKeys
newOperation.queryCompletionBlock = queryOperation.queryCompletionBlock
newOperationQueue.addOperation(newOperation)
}
}
queryOperation.queryCompletionBlock = queryCompletionBlock
queryOperationQueue.addOperation(queryOperation)

Understanding how to correctly execute CKQueryOperation

With the new async pattern it has become much easier to fetch data from CloudKit.

Instead of CKQueryOperation you call records(matching:resultsLimit:) directly and map the result to whatever you like.

A possible error is handed over to the caller.

func queryAllNotes() async throws -> [(title: String, cloudID: String)] {
//set the cloud database to .publicCloudDatabase
let container = CKContainer.default()
let cloudDB = container.publicCloudDatabase

let pred = NSPredicate(value: true) //true -> return all records
let query = CKQuery(recordType: "Notes", predicate: pred)

let (notesResults, _) = try await cloudDB.records(matching: query,
resultsLimit: 100)
return notesResults
.compactMap { _, result in
guard let record = try? result.get(),
let noteTitle = record["Title"] as? String else { return nil }
return (title: noteTitle, cloudID: record.recordID.recordName)
}
}

And use it

override func viewDidLoad() {
super.viewDidLoad()

// do additional setup here

// set serachField delegate
searchField.delegate = self

// set tableView delegate and data source
tableView.delegate = self
tableView.dataSource = self

// load all NoteRecords in public cloud db into noteRecords
Task {
do {
noteRecords = try await queryAllNotes()
tableView.reloadData()
} catch {
print(error)
}
}

}

Please watch the related video from WWDC 2021 for detailed information about the async CloudKit APIs and also the Apple examples on GitHub.

Side note:

Rather than a tuple use a struct. Tuples as data source array are discouraged.

Why will my CKQueryOperation only return a Cursor if the results limit is less than 1000?

I believe that 400 is the the limit for a single operation, so you need to use cursor to get more records, and keep on doing that while returned cursor is not nil.

See how it is done in RxCloudKit library' RecordFetcher -
https://github.com/maxvol/RxCloudKit/blob/master/RxCloudKit/RecordFetcher.swift

CKQuery from private zone returns only first 100 CKRecords from in CloudKit

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.

How to use query to fetch cloudkit data in swift

I believe instead of:

loadLocation((error, self.locArray))

You need to call this:

loadLocation() { (error, records) in
// do something here with the returned data.
}

Why is CloudKit on Apple Watch super slow?

Concept

qualityOfService is set to default when you don't assign a configuration.

Assume the watch is low on battery then system decides whether to process the operation immediately or later.

So setting it explicitly might help the system determine how you would like the operation to be handled.

Code

Can you try setting the configuration as follows:

let configuration = CKOperation.Configuration()
configuration.qualityOfService = .userInitiated
queryOperation.configuration = configuration

queryOperation.queuePriority = .veryHigh //Use this wisely, marking everything as very high priority doesn't help

CloudKit private database returns first 100 CKRecords

Ok, i found a solution. See below:

func loadDataFromCloudKit() {
var results: [AnyObject] = []
let cloudContainer = CKContainer.defaultContainer()
let privateDatabase = cloudContainer.privateCloudDatabase

let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Data", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]

let queryOperation = CKQueryOperation(query: query)
queryOperation.desiredKeys = ["id","name"]
queryOperation.queuePriority = .VeryHigh
// Max limit is still 100
queryOperation.resultsLimit = 100

queryOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
results.append(record)
}

queryOperation.queryCompletionBlock = { (cursor, error) in
dispatch_async(dispatch_get_main_queue()) {
if (error != nil) {
print("Failed to get data from iCloud - \(error!.localizedDescription)")
} else {
print("Successfully retrieve the data form iCloud")

}
}
// see cursor here, if it is nil than you have no more records
// if it has a value than you have more records to get
if cursor != nil {
print("there is more data to fetch")
let newOperation = CKQueryOperation(cursor: cursor!)
newOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
results.append(record)
}

newOperation.queryCompletionBlock = queryOperation.queryCompletionBlock
privateDatabase.addOperation(newOperation)
} else {
// gets more then 100
print(results.count)
}
}
privateDatabase.addOperation(queryOperation)
}


Related Topics



Leave a reply



Submit