I'm learning to use CloudKit and finally managed to retrieve/query data hard coded into the dashboard. I'm trying to query only one value from a record using a CKQueryOperation but I always receive all the values in each and every record.
In my code below I'm trying to get just the "age" values by using the desiredKey property - what I get back is "age", "firstName", "lastName" etc etc.
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
// select all records
let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)
let query = CKQuery(recordType: "quizRecord", predicate: predicate)
// get just one value only
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["age"]
// addOperation
publicData.addOperation(operation)
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
// PROBLEM .. this prints out all values ????
print(results)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
})
}
else {
print(error)
}
}
//// The following / amended code works fine
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
// select all records
let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)
let query = CKQuery(recordType: "quizRecord", predicate: predicate)
// get just one value only
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["age"]
// get query
operation.recordFetchedBlock = { (record : CKRecord) in
// process record
print(record)
}
// operation completed
operation.queryCompletionBlock = {(cursor, error) in
dispatch_async(dispatch_get_main_queue()) {
if error == nil {
print("no errors")
// code here
} else {
print("error description = \(error?.description)")
}
}
}
// addOperation
publicData.addOperation(operation)
The problem is simple. You set the desiredKeys on the CKQueryOperation but you never actually use the operation. You instead use performQuery on the database.
If you to use the operation and limit the data to the specific keys, you need to add the operation to the database (using addOperation:) instead of calling performQuery. And of course you need to assign a block to the one or more of the recordFetchedBlock and queryCompletionBlock properties of the operation.
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
// select all records
let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)
let query = CKQuery(recordType: "quizRecord", predicate: predicate)
// get just one value only
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["age"]
operation.recordFetchBlock = { (record : CKRecord) in
// process record
}
operation.queryCompletionBlock = { (cursor : CKQueryCursor!, error : NSError!) in
// Query finished
}
// addOperation
publicData.addOperation(operation)
Related
How would I query CloudKit for a specific record by its recordName?
The following code gives me an error.
let recordName = "BBBA9068-D01A-9D55-3D6B-4460A7B36D57"
let predicate = NSPredicate(format: "recordID=%#", recordName)
let query = CKQuery(recordType: "Route", predicate: predicate)
privateDatabase.perform(query, inZoneWith: nil) {
(records: [CKRecord]?, error: Error?) in
if error != nil {
print(error!.localizedDescription)
} else {
if let records = records {
print(records.first as Any)
}
}
}
The debug window shows:
Field '___recordID' has a value type of REFERENCE and cannot be queried using filter value type STRING
I changed the first two lines of the code and got it to work.
let recordID: CKRecord.ID = CKRecord.ID(recordName: "BBBA9068-D01A-9D55-3D6B-4460A7B36D57")
let predicate = NSPredicate(format: "recordID=%#", recordID)
let query = CKQuery(recordType: "Route", predicate: predicate)
privateDatabase.perform(query, inZoneWith: nil) {
(records: [CKRecord]?, error: Error?) in
if error != nil {
print(error!.localizedDescription)
} else {
if let records = records {
print(records.first as Any)
}
}
}
GenerateCKRecord.ID by your recordName
For one recordName you can do like that:
let recordID = CKRecord.ID(recordName: "recordName", zoneID: "Zone")
if you don't use zones, will automatically use the default zone
let recordID = CKRecord.ID(recordName: "recordName")
For multiple recordIDs:
let recordIDs = recordNames.map { CKRecord.ID(recordName: $0) }
If you want to use predicate:
let recordIDs = recordNames.map { CKRecord.ID(recordName: $0) }
let predicate: NSPredicate = NSPredicate(format: "recordID IN %#", recordIDs)
Hopefully my answer meet your needs
How would I declare a closure that retrieves records from CloudKit that iteratively calls itself until all the records are fetched? The following code does that, except I get an error saying
Variable used within its own initial value
at the line where I assign the closure queryCompletionBlock to the property by that same name of the query operation within the declaration of the closure itself. Basically, I get an error because I call the closure within itself in the declaration of the closure before the closure is declared.
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 queryOperation = CKQueryOperation(cursor: cursor)
queryOperation.desiredKeys = [DatabaseNameStrings.fieldNameCreatedAt, DatabaseNameStrings.fieldNameText]
queryOperation.queryCompletionBlock = queryCompletionBlock // Error message "Variable used within its own initial value"
queryOperationQueue.addOperation(queryOperation)
}
}
queryOperation.queryCompletionBlock = queryCompletionBlock
queryOperationQueue.addOperation(queryOperation)
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)
I want to fetch only Song_Name field in Musics records in CloudKit. I'm trying below code but It still fetches all the fields in Music record. I think I need to compile operation with publicDB.add(operation) method but It doesn't allow me to declare ¨results¨ as I do now with publicDB.perform(query, in....)
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Musics", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["Song_Name"]
publicDB.perform(query, inZoneWith: nil) { [unowned self] results, error in
guard error == nil else {
DispatchQueue.main.async {
self.delegate?.errorUpdating(error! as NSError)
print("Cloud Query Error - Refresh: \(error)")
}
return
}
self.items_music.removeAll(keepingCapacity: true)
for record in results! {
let music = Musics(record: record, database: self.publicDB)
self.items_music.append(music)
}
DispatchQueue.main.async {
self.delegate?.modelUpdated()
}
}
}
You are creating a CKQueryOperation and setting the operation's desiredKeys property, but then you are not using this operation. You simply perform a query against the database, so none of the query operation's properties will be used.
You need to assign a record fetched block and query completion block to your operation and then add the operation to the database to actually run it.
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Musics", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["Song_Name"]
var newItems = [Musics]()
operation.queryCompletionBlock = ( { (cursor, error)->Void in
guard error == nil else {
DispatchQueue.main.async {
self.delegate?.errorUpdating(error! as NSError)
print("Cloud Query Error - Refresh: \(error)")
}
return
}
self.items_music = newItems
DispatchQueue.main.async {
self.delegate?.modelUpdated()
}
})
operation.recordFetchedBlock = ( { (record) -> Void in
let music = Musics(record: record, database: self.publicDB)
newItems.append(music)
})
publicDB.add(operation)
I have omitted some code that you will need that checks the cursor provided to the completion block. If this isn't nil then you need to issue another query to fetch additional items
i am facing this kind of problem working with CloudKit. Trying to fetch all data from "Data" record. Result is limited by 100. How to get all data? Please, thank for any advice.
func getAllDataFromCloudKit(){
let predicate = NSPredicate(value: true)
let container = CKContainer.defaultContainer()
let privateDatabase = container.privateCloudDatabase
let query = CKQuery(recordType: "Data", predicate: predicate)
privateDatabase.performQuery(query, inZoneWithID: nil) { results, error in
if error != nil {
print(error)
}
else {
for result in results! {
// return only 100 first
}
}
}
}
P.S. i found one similar question, still not clear or answer is too old and does not work with the new Swift version
EDIT: See my final solution how to get all data from private database below:
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)
}
Kevin,
You use the cursor returned by CKQueryOperation; it is very much a standard approach in the world of databases; I know some dropbox operations use the same approach for example. Here is the basic code for a CKQueryOperation.
func query4Cloud(theLink: String, theCount: Int) {
var starCount:Int = 0
let container = CKContainer(identifier: "iCloud.ch")
let publicDB = container.publicCloudDatabase
let singleLink2LinkthemALL = CKRecordID(recordName: theLink)
let recordToMatch = CKReference(recordID: singleLink2LinkthemALL, action: .DeleteSelf)
let predicate = NSPredicate(format:"theLink == %#", recordToMatch)
let query = CKQuery(recordType: "Files", predicate: predicate)
// You run the query operation replacing query with your cursor
readerOperation = CKQueryOperation(query: query)
readerOperation.desiredKeys = ["record.recordID.recordName"];
readerOperation.recordFetchedBlock = { (record) in
starCount += 1
}
// 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
readerOperation.queryCompletionBlock = {(cursor, error) in
print("fcuk query4Cloud \(theLink) \(theCount) \(starCount)" )
if error != nil {
self.showAlert(message: error!.localizedDescription)
print("ting, busted")
} else {
// it's done
}
}
print("publicDB.addOperation(operation)")
readerOperation.qualityOfService = .Background
publicDB.addOperation(readerOperation)
}
Say I have a 'Species' Record Type that contains Public Records which were created by a number of Users. Currently, my query retrieves all records of 'Species':
private func fetchSpecies() {
// Fetch Public Database
let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase
// Initialize Query
let query = CKQuery(recordType: "Species", predicate: NSPredicate(format: "TRUEPREDICATE"))
// Configure Query
query.sortDescriptors = [NSSortDescriptor(key: "latinName", ascending: true)]
// Perform Query
publicDatabase.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Process Response on Main Thread
self.processResponseForQuery(records, error: error)
})
}
}
How can I only fetch records that were created by the current user (as in the owner of the device)?
Thank you!
I also posted on the Apple Developer Forums, and I was provided with an excellent solution:
To fetch only a specific user records, you need to create a CKReference pointing to that user id (recordID), then use a predicate on that. It looks like this:
First fetch the user record id
yourContainer.fetchUserRecordIDWithCompletionHandler { (userID, error) -> Void in
if let userID = userID {
// here's your userID (recordID) to play with
}
}
Then construct the predicate:
let reference = CKReference(recordID: userID, action: .None)
let predicate = NSPredicate(format: "creatorUserRecordID == %#", reference)
let query = CKQuery(recordType: "Species", predicate: predicate)
And then use the query as normal.
Happy fetching!
Each CKRecord has a creatorUserRecordID property; therefore, you can try to get owner's userRecordID first. Then, have it into NSPredicate.
let container: CKContainer = CKContainer.defaultContainer()
let completionHandler: (CKRecordID?, NSError?) -> Void = { (userRecordID: CKRecordID?, error: NSError?) in
if let userRecordID = userRecordID {
let predicate = NSPredicate(format: "creatorUserRecordID == %#", userRecordID)
let query = CKQuery(recordType: "Species", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "latinName", ascending: true)]
container.publicCloudDatabase.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
}
}
}
//// Returns the user record ID associated with the current user.
container.fetchUserRecordIDWithCompletionHandler(completionHandler)