Welcome to the Treehouse Community

The Treehouse Community is a meeting place for developers, designers, and programmers of all backgrounds and skill levels to get support. Collaborate here on code errors or bugs that you need feedback on, or asking for an extra set of eyes on your latest project. Join thousands of Treehouse students and alumni in the community today. (Note: Only Treehouse students can comment or ask questions, but non-students are welcome to browse our conversations.)

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and a supportive community. Start your free trial today.

iOS

Simen Anthonsen
Simen Anthonsen
1,097 Points

CloudKit query

I have got a record type named Movies, and in my Public Data Default Zone I have created 10 movie objects. My objective is to query all 10 objects and append them to an array.

How do I do this?

3 Answers

var moviesArray: [CKRecord] = []

func getCloudKitMoviesData() {
  let cloudContainer = CKContainer.defaultContainer()
            let publicDatabase = cloudContainer.publicCloudDatabase
            let predicate = NSPredicate(value: true)
            let query = CKQuery(recordType: "Movies", predicate: predicate)

            //operational API, you can use convenience API but I've never used it in a project since you're generally just pulling      certain things.
            let queryOperation = CKQueryOperation(query: query)
           //ADD DESIRED KEYS IN ARRAY BELOW
            queryOperation.desiredKeys = ["", "", ""]
            queryOperation.queuePriority = .VeryHigh
            queryOperation.recordFetchedBlock = {
                (record: CKRecord!) -> Void in
                if let moviesRecord = record {
                //this is where you are appending to your array
                    self.moviesArray.append(moviesRecord)
                }
            }
            queryOperation.queryCompletionBlock = {
                (cursor:CKQueryCursor?, error: NSError?) -> Void in
                //if there is an error, throw an error controller
                if (error != nil) {
                    let controller = UIAlertController(title: "Error", message: "Error: \(error)", preferredStyle: .Alert)
                    let action = UIAlertAction(title: "OK", style: .Default, handler: nil)
                    controller.addAction(action)
                    self.presentViewController(controller, animated: true, completion: nil)
                }
                NSOperationQueue.mainQueue().addOperationWithBlock() {
                  //Do anything else with the record after downloaded like assigning to properties or variables
                    }
                }
            }
            publicDatabase.addOperation(queryOperation)
        }

So let me walk you through this.

Don't forget to import CloudKit on top.

  • Cloud Container: You first want to grab your container so you assign some constant (in this case cloudContainer) to the default container. If you have multiple apps using the same containers you can assign it like so:
let cloudContainer = CKContainer(identifier: "iCloud.com.yourIdentifierName.yourAppName")
  • Public/ Private Database: Assign either the public or private database to a constant to use later. Access the private container the same way using dot syntax. Just use .privateCloudDatabase instead.

  • Predicate: Allows you to grab only certain record that meet certain criteria. I've used it in the simplest sense in a stock exchange app to grab only a certain record that equals a stock name. For example:

let predicate = NSPredicate(format: "stockSymbol = %@", stockSymbol)

where stockSymbol is a predefined variable. If you're just looking for them all, you can just define it like above with it's value set to true.

  • query: Create a CKQuery with your recordType name. In your case it's Movies. It's whatever the main titles of your record types are in the CK Dashboard.

  • query operation: Here's your actual operation. You wanted to append it to a new array so create an array outside the function (so you can use it later), set it empty, and append the results from the cloud in a fetchedBlock. Make sure to define your keys you want (the "field names" in your record types in you CK Dashboard). There is a convenience API as well where it just downloads everything from that recordType but rarely have I done this. IMO it's better to learn the operational as you'll have more use for it in your apps. In the completion, make sure to be able to handle errors and if no errors, do something (stop activity spinner, assign labels, etc.).

Don't forget to add the operation and run it to the publicDatabase contestant you created earlier. Don't forget to also run the function (generally in viewDidLoad). I've done that many times haha!

Simen Anthonsen
Simen Anthonsen
1,097 Points

Thank you so much!!

One extra question: How do I access the properties/keys to the movie objects after they have been retrieved? My keys/field names include Title, Genre and rating. Like this.

myLabel.text = moviesArray[0].title

how do I manage this?

Simen Anthonsen
Simen Anthonsen
1,097 Points

Hi again! You wouldn´t know how to skip past the standard 100 limit when fetching records?

Simen Anthonsen
Simen Anthonsen
1,097 Points

Thank you so much!!

One extra question: How do I access the properties/keys to the movie objects after they have been retrieved? My keys/field names include Title, Genre and rating. Like this.

myLabel.text = moviesArray[0].title

how do I manage this?

Using the moviesArray from before:

moviesArray[index].objectForKey("title") as? String

where the index is whatever index value you want to access, the "title" is the key name (could be genre, rating, etc) which you then cast as whatever you created in the CK Dashboard. Let's say you were building a tableView list of each movie with their properties and you set up labels like so:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! MoviesTableViewCell

let movieReuseIndex = moviesArray[indexPath.row]
cell.genreLabel.text = movieReuseIndex.objectForKey("genre") as? String

// or use it with string interpolation
let rating = movieReuseIndex.objectForKey("rating") as? Int
cell.ratingLabel.text = "\(rating) stars"

return cell
}

From the documentation

"If the search yields many records, the operation object may deliver a portion of the total results to your blocks immediately, along with a cursor for obtaining the remaining records. If a cursor is provided, use it to initialize and execute a separate CKQueryOperation object when you are ready to process the next batch of results."

This is a good code example of it.

Simen Anthonsen
Simen Anthonsen
1,097 Points

Thanks man, I have looked at it. I just have a hard time understanding it :S

CloudKit's documentation is awful but can provide at least some clues generally. CK is just not old enough for every question to be answered. I actually got an app rejected 4 times because CK didn't work in the reviewers environment but worked in every other production capacity. I even got DTS involved and they couldn't replicate the problem. I would love to use it more but I definitely understand Apple doesn't make it simple.

Simen Anthonsen
Simen Anthonsen
1,097 Points

Yeah I know. Look, I don´t want to be rude or bugging you, but if you know how to modify the code you gave me, the one listed as the answer of this thread, to make sure it retrieves all of my records I would greatly appreciate it. This is literally the only thing I have got left to do before submitting my first ever app.