I have currently got CloudKit set up in my app so that I am adding a new record using the help of the following code below,
CKRecordID *recordID
Swift 5
After looking through a bunch of posts and solutions on SO I have managed to come with a solution that suits my needs and should be simple enough for anyone that just wants to fetch all of their records of given type from iCloud.
The solution that uses an extension to the CKDatabase to introduce a method that handles the cursor: CKQueryOperation.Cursor
of CKQueryOperation
to continue asking iCloud for more records. In this approach I dispatch to the background queue so I can block it and wait for the operation to be finished completely, either on receiving an error or with the last batch of records. Once the semaphore unlocks the queue it proceeds with calling the main completion block with the result. Also I am taking advantage of Swift's Result
type in the completion handler.
extension CKDatabase {
func fetchAll(
recordType: String, resultsLimit: Int = 100, timeout: TimeInterval = 60,
completion: @escaping (Result<[CKRecord], Error>) -> Void
) {
DispatchQueue.global().async { [unowned self] in
let query = CKQuery(
recordType: recordType, predicate: NSPredicate(value: true)
)
let semaphore = DispatchSemaphore(value: 0)
var records = [CKRecord]()
var error: Error?
var operation = CKQueryOperation(query: query)
operation.resultsLimit = resultsLimit
operation.recordFetchedBlock = { records.append($0) }
operation.queryCompletionBlock = { (cursor, err) in
guard err == nil, let cursor = cursor else {
error = err
semaphore.signal()
return
}
let newOperation = CKQueryOperation(cursor: cursor)
newOperation.resultsLimit = operation.resultsLimit
newOperation.recordFetchedBlock = operation.recordFetchedBlock
newOperation.queryCompletionBlock = operation.queryCompletionBlock
operation = newOperation
self?.add(newOperation)
}
self?.add(operation)
_ = semaphore.wait(timeout: .now() + 60)
if let error = error {
completion(.failure(error))
} else {
completion(.success(records))
}
}
}
}
Using the method is fairly straight forward for anyone familiar with Swift's closure syntax and Result
type.
let database: CKDatabase = ...
database.fetchAll(recordType: "User") { result in
switch result {
case .success(let users):
// Handle fetched users, ex. save them to the database
case .failure(let error):
// Handle Error
}
}
}
The followig function will return ALL records for requested record type:
let database = CKContainer(identifier: "container_here").privateCloudDatabase
typealias RecordsErrorHandler = ([CKRecord], Swift.Error?) -> Void
func fetchRecords(forType type: String, completion: RecordsErrorHandler? = nil) {
var records = [CKRecord]()
let query = CKQuery(recordType: type, predicate: NSPredicate(value: true))
let queryOperation = CKQueryOperation(query: query)
queryOperation.zoneID = CloudAssistant.shared.zone.zoneID
queryOperation.recordFetchedBlock = { record in
records.append(record)
}
queryOperation.queryCompletionBlock = { cursor, error in
self.fetchRecords(with: cursor, error: error, records: records) { records in
completion?(records, nil)
}
}
database.add(queryOperation)
}
private func fetchRecords(with cursor: CKQueryCursor?, error: Swift.Error?, records: [CKRecord], completion: RecordsHandler?) {
var currentRecords = records
if let cursor = cursor, error == nil {
let queryOperation = CKQueryOperation(cursor: cursor)
queryOperation.recordFetchedBlock = { record in
currentRecords.append(record)
}
queryOperation.queryCompletionBlock = { cursor, error in
self.fetchRecords(with: cursor, error: error, records: currentRecords, completion: completion)
}
database.add(queryOperation)
} else {
completion?(records)
}
}
Aaaand, I've got it. Using the code below, I was able to create a query to run on the database, to then return an NSArray in the completion block, which I looped through, and returned the value for the saved key in an NSLog.
NSPredicate *predicate = [NSPredicate predicateWithValue:YES];
CKQuery *query = [[CKQuery alloc] initWithRecordType:@"Strings" predicate:predicate];
[_privateDatabase performQuery:query inZoneWithID:nil completionHandler:^(NSArray *results, NSError *error) {
for (CKRecord *record in results) {
NSLog(@"Contents: %@", [record objectForKey:@"stringArray"]);
}
}];
Solution for Swift 4,
shows how to fetch all the records of type "YourTable", also prints System Field
and Custom Field
:
let query = CKQuery(recordType: "YourTable", predicate: NSPredicate(value: true))
CKContainer.default().publicCloudDatabase.perform(query, inZoneWith: nil) { (records, error) in
records?.forEach({ (record) in
// System Field from property
let recordName_fromProperty = record.recordID.recordName
print("System Field, recordName: \(recordName_fromProperty)")
// Custom Field from key path (eg: deeplink)
let deeplink = record.value(forKey: "deeplink")
print("Custom Field, deeplink: \(deeplink ?? "")")
})
}
Here's the answer in Swift 3.0.
func myQuery() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "tableName", predicate: predicate)
publicDatabase.perform(query, inZoneWith: nil) { (record, error) in
for record: CKRecord in record! {
//...
// if you want to access a certain 'field'.
let name = record.value(forKeyPath: "Name") as! String
}
}
}
In trying to fetch all records, and understand the structure and details of Cloudkit storage, I found it useful to have the following function available during debug. This uses semaphores to retain the data structure for printing. There may be a more elegant way to do this but this works!
//
// Print a list of records in all zones in all databases
//
func printRecordsInContainers() {
let myContainer = CKContainer.default()
// Edit the following dictionary to include any known containers and possible record types
let containerRecordTypes: [CKContainer: [String]] = [ myContainer: ["MyRecordType", "OldRecordType", "MyUser", "PrivateInfo"] ]
let containers = Array(containerRecordTypes.keys)
for containerz in containers {
let databases: [CKDatabase] = [containerz.publicCloudDatabase, containerz.privateCloudDatabase, containerz.sharedCloudDatabase]
for database in databases {
var dbType = "<None>"
if database.databaseScope.rawValue == 1 { dbType = "Public" }
if database.databaseScope.rawValue == 2 { dbType = "Private" }
if database.databaseScope.rawValue == 3 { dbType = "Shared" }
//print("\(database.debugDescription)")
print("\n\n\n