Search code examples
swiftcloudkitckqueryckqueryoperation

Understanding how to correctly execute CKQueryOperation


I'm working with CloudKit for the first time and am having trouble executing a CKQueryOperation to query all records of a given type. It doesn't help that Apple has deprecated most of the stuff I've found online and that their documentation for these things are completely blank besides the func declaration. I think I've got the "skeleton" of the code done but am unsure of what goes into the .recordMatchedBlock and the .queryResultsBlock.

I have a func queryAllNotes() which should query all records in the public database of type "Notes" and return an array of tuples of the note's title and its associated cloudID, which is just the unique recordName given to it when it is added to the database.

Here's the code for queryAllNotes() :

private func queryAllNotes() -> [(title: String, cloudID: String)] {
    /*
     query all notes in the cloud DB into an array to populate
     the tableView
     */
    var resultArray: [(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 queryOperation = CKQueryOperation(query: query)
    queryOperation.database = cloudDB
    queryOperation.resultsLimit = 100
    
    queryOperation.recordMatchedBlock = { (record: CKRecord) in
        let noteTitle = record["Title"] as! String
        let noteCloudID = record.recordID.recordName
        
        resultArray.append((noteTitle, noteCloudID))
    }
    
    queryOperation.queryResultBlock = { (cursor, error) in
        
        
    }
    
    return resultArray
}

To my understanding the .recordMatchedBlock is called for every record returned by the query so I think it is complete but I could be very wrong. In regards to the .queryResultBlock, my understanding is that the query technically only return one record at a time and this block basically tells the query to run again for the next record for all records within the .resultLimit. How can I structure this query? I am keen to understand what each of these blocks do.

Also this is for a macOS app; I don't know if the code is different for macOS vs iOS but I thought I should include this just in case.

Also I'm getting an error saying "Type of expression is ambiguous without more context" which I'm assuming is because I haven't completed setting up my query. If it's for a different reason could also explain why this is happening.

Edit

I call this func inside of viewDidLoad() like so:

//array var for the array that is used to populate the tableView
var noteRecords: [(title: String, cloudID: String)] = []

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
    
    noteRecords = queryAllNotes()
    
}

Solution

  • 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.