Code pattern / snippet for retrieving single column from cloudkit - ios

I'm building an app which allows users to upload / download info from a common store. I thought cloudKit storage would be ideal for this.
I'd like for users to be able to search records in the store by a KeyWord field, then download the entire record when they select one.
In SQL terms this would be like:
SELECT id,KeyWords from myDB WHERE KeyWords LIKE %searchstring%
Followed by:
SELECT * FROM myDB WHERE id = selectedID
I have been using this code pattern to retrieve records from cloudKit storage:
var publicDatabase: CKDatabase?
let container = CKContainer.defaultContainer()
override func viewDidLoad() {
super.viewDidLoad()
publicDatabase = container.publicCloudDatabase
}
func performQuery(){
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Library", predicate: predicate)
publicDatabase?.performQuery(query, inZoneWithID: nil,
completionHandler: ({results, error in
...
[code to handle results / error here]
...
})
}
but this returns all the content of each record which is a lot of unnecessary info.
I'd like to only fetch a single field from the cloudkit records.
Is there an easy way to do this and does anyone have a code snippet?
CKFetchRecordsOperation allows downloading of restricted columns but requires that you know the ids of the records to download.

For this you can use the desiredKeys property on a CKQueryOperation. For more information see: desiredkeys documentation

OK Got something working. This may well be hacky but I put it up for info for anyone else in the same situation...
let predicate = NSPredicate(value: true) // returns all - replace with whatever condition you want
let query = CKQuery(recordType: "Library", predicate: predicate) // create a query using the predicate
var operation = CKQueryOperation(query: query) // create an operation using the query
operation.desiredKeys = ["KeyWords"] // Array of whatever 'columns' you want to return
// operation.resultsLimit = 15 // optional limit on records
// Define a closure for what to do for each returned record
operation.recordFetchedBlock = { [weak self] (record:CKRecord!) in
// Do whatever you want with each returned record
println(record.objectForKey("KeyWords"))
}
// Define a closure for when the operation is complete
operation.queryCompletionBlock = { [weak self] (cursor:CKQueryCursor!, error:NSError!) in
if cursor != nil {
// returns the point where the operation left off if you want t retrieve the rest of the records
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Do what you want when process complete
self!.tableView.reloadData()
if error != nil {
println("there was an error")
}
})
}
self.publicDatabase!.addOperation(operation) // Perform the operation on given database

Related

Is it possible to perform NSBatchUpdateRequest with an array of NSManagedObjectID?

Currently, I perform multiple update operations via the following code.
func updateOrders(_ updates : [(objectID: NSManagedObjectID, order: Int64)]) {
if updates.isEmpty {
return
}
let coreDataStack = CoreDataStack.INSTANCE
let backgroundContext = coreDataStack.backgroundContext
backgroundContext.perform {
for update in updates {
let objectID = update.objectID
let order = update.order
let nsPlainNote = try! backgroundContext.existingObject(with: objectID) as! NSPlainNote
nsPlainNote.order = order
}
RepositoryUtils.saveContextIfPossible(backgroundContext)
}
}
Since I would like to
Make the update operations run faster
Avoid delegate of NSFetchedResultController from being notified
I would like to utilise NSBatchUpdateRequest for performing such update operation.
However, I don't find a way, how I can apply array of NSManagedObjectID and array of Int64 value, to NSBatchUpdateRequest.
Given an array of NSManagedObjectID and Int64, is it possible to use NSBatchUpdateRequest to perform updated on CoreData?
You must use NSPredicate to set object id
func updateOrders(_ updates : [(objectID: NSManagedObjectID, order: Int64)]) {
updates.forEach {
let request = NSBatchUpdateRequest(entityName: "NSPlainNote")
request.propertiesToUpdate = ["order": $0.order]
request.predicate = NSPredicate(format: "objectID == %#", $0.objectID)
let result = try? context.execute(request)
}
}
NSBatchUpdateRequest is not suitable for your task since using it makes sense for large amount of records with a common attribute's value so that you can filter all by your criteria and update all fields with your values at once.
The fact is that the NSBatchDeleteRequest is an NSPersistentStoreRequest which operates at the SQL level in the persistent store itself and it doesn't update your in-memory objects after execution thats why it works so fast and Core Data translates your native requests to a SQL ones where you can not use dynamically code to get and insert needed data from dictionary etc. but you can update the current value of a filed e.g.:
let batchRequest = NSBatchUpdateRequest(entityName: "Note")
batchRequest.predicate = predicate
// Increase `order` value
batchRequest.propertiesToUpdate = ["order" : NSExpression(format: "order + 1")]
do {
try context.execute(batchRequest)
}
catch {
fatalError(error.localizedDescription)
}

Unable to fetch records in a sharedCloudDatabase custom Zone using CloudKit

I am trying to fetch CloudKit records from a custom Zone in a sharedDatabase.
The zone has been created correctly during the share process. So I assume that the zone is correctly a shared custom zone (it is indeed in my CloudKit user dashboard, appearing under the sharedDatabase of the default container).
Even with this simple piece of code to retrieve records:
func loadRecords() {
let database = CKContainer.default().sharedCloudDatabase
let query = CKQuery(recordType: "Items", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
let operation = CKQueryOperation(query: query)
let zone = CKRecordZone(zoneName: "MyCustomZone")
var fetchedRecords: [CKRecord] = []
database.perform(query, inZoneWith: zone.zoneID) { (records, error) in
if let error = error {
print(error)
}
else if let records = records, let firstRecord = records.first {
print(firstRecord)
}
}
}
I keep receiving this error: (sorry was missing in previous post!)
<CKError 0x280950840: "Invalid Arguments" (12/1009); "Only shared zones can be accessed in the shared DB">
Any idea about what I am missing? Thank you!
Right, I finally figured it out. Thanks to this additional post here on StackOverflow: CloudKit Sharing
You indeed have to fetch your custom zone. But it is not enough to give it a name such as "MyCustomZone" because in your shared DB, it becomes MyCustomZone: _acBxxxyyyzzz. A suffix added by CloudKit.
If you don't fetch all your shared zones, you never have the right custom zone from which you need to pickup the record.

CloudKit does not retrieve record that should be there and retrievable

I have a function that creates a new record in CloudKit. Once that record was successfully created it calls a function called refresh() which will call some other function that queries for the record I just created. However, it says I created the record, but it never goes inside of the next query which is telling me that the new record may not be reaching CloudKit in time before it is being queried for. Incidentally, if I were to look in the database, the record WAS created correctly.
Creating new record function:
func saveCreatedGroup2(){
let Group = CKRecord(recordType: "Group")
print(groupName)
Group.setObject(groupName as CKRecordValue?, forKey: "groupName")
Group.setObject(groupDesc as CKRecordValue?, forKey: "groupDesc")
Group.setObject(publicOrPrivate as CKRecordValue?, forKey: "PublicOrPrivate")
Group.setObject(members as CKRecordValue?, forKey: "memberUsernames")
Group.setObject(leaders as CKRecordValue?, forKey: "leaders")
database.save(Group) { (savedRecord, error) in
if error != nil{
print(error.debugDescription)
}else{
DispatchQueue.main.async{
print("SAVED RECORD, CREATED NEW GROUP!!!!")
// IT DOES SAY THIS^
thisTeamName = selectedCellName
createdNewGroup = false
self.refresh()
}
}
}
}
refresh() function:
func refresh(){
tempThisGroupTuple = []
sortedTupleArray = []
otherTeam = []
otherSortedArray = []
tempOtherTeamTuple = []
canSwitchViews = false
if (canRefresh == false){
print ("ABANDONED")
refreshControl.endRefreshing()
}else{
loadGroupPage()
DispatchQueue.main.async{
self.requestsTableView.reloadData()
self.groupFeedTableView.reloadData()
}
}
}
loadGroupPage() (querying for just created record) function - prints line that is before query, but does not print line that is inside query!:
func loadGroupPage() {
print("loadingf group pagew...\(thisTeamName)") // PRINTS THIS
let pred = NSPredicate(format: "groupName = %#", thisTeamName)
let query = CKQuery(recordType: "Group", predicate: pred)
let operation = CKQueryOperation(query: query)
operation.qualityOfService = .userInteractive
var canJoin = false
var requestVisible = false
operation.recordFetchedBlock = { (record: CKRecord!) in
if record != nil{
self.OurGroup = record
groupMembs = (record.object(forKey: "memberUsernames") as! Array) // loads group members
print("original groupMembs: \(groupMembs)") // DOES NOT PRINT THIS LINE, SO IT IS TELLING ME IT DID NOT FIND THE RECORD
...(There's more to this function but it's unnecessary)
I also confirmed that when the query begins, the predicate/"thisTeamName" is supposed to be what it is, which is the groupName of the record just created. I am confused on why it is not able to retrieve/query for the record.
Your refresh function calls loadGroupPage() then immediately fires off an async call to reload the tables without waiting for the loadGroupPage function to complete so even if you do get a record I don't think it's going to work properly.
You don't show in the loadGroupPage function where you actually add your request operation to a database. I'm assuming that's in there?
The recordFetchedBlock will fire once for each record returned, you don't need to force unwrap the record (record: CKRecord!). You also don't need to check record != nil since it's guaranteed to be there.
The CKQuery recordFetchedBlock docs say:
Warning
Query indexes are updated asynchronously so they are not guaranteed to be current. If you query for records that you recently changed and not allow enough time for those changes to be processed, the query results may be incorrect. The results may not contain the correct records and the records may be out of order.
I'm not an expert by any means (and haven't used the convenience API) but have built syncing into my app and just from my experience I'd be surprised if the problem is the index hasn't updated.
I would recommend adding in a queryCompletionBlock and check for errors there. You should always be checking for errors when using CloudKit because even when the code is right there will still be times when you get errors (network unavailable, iCloud down, etc.)

CloudKit cursors returning more results than expected

I am trying to query all the records of a specified RecordType in my CloudKit database. Because I may have more than 100 records of that type, I am using CloudKit cursors, which return records in batches until there are no more records to return (at which point the cursor is nil).
I save the number of records created on each creationDate. I am getting much higher numbers for each date than there are records in the CloudKit dashboard for those dates. For example, on one day when I count 110 records by hand on the CloudKit dashboard, my code says there are 256. On another day where I count 2, my code says there are 7.
Issues I've considered
Time zones differences - unlikely because these high counts occur even on days where there are no records created for several days before and after.
Maybe I'm counting the same records multiple times - I printed out the list of all 472 records that CloudKit found across all the cursor queries. From some testing, it would appear that there are not any record ID duplicates in that list, and all of the records are of the proper type. (Which seems so weird that I wonder if I need to look at that list more.)
Maybe there's something I'm misunderstanding with my use of cursors. Hence, this StackOverflow post.
Here's my code for how I'm working with cursors. If anyone sees anything off, please let me know!
// This is the main code that kicks off the cursor handling
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: recordType, predicate: predicate)
let sort = NSSortDescriptor(key: "creationDate", ascending: true) // so the 0th result is the earliest
query.sortDescriptors = [sort]
let operation1 = CKQueryOperation(query: query)
operation1.resultsLimit = 5
operation1.queryCompletionBlock = { (cursor, error) in
if error != nil {
self.recordTypeErrorHandling(error: error as! CKError, uid: uid, appID: appID, recordType: recordType)
}
else {
self.queryRecordsWithCursor(cursor: cursor, isFirstCheck: true, uid: uid, appID: appID, recordType: recordType)
}
CKContainer.default().publicCloudDatabase.add(operation1)
// Related functions
// adapted from: https://gist.github.com/evermeer/5df7ad1f8db529893f40
func queryRecordsWithCursor(cursor: CKQueryCursor?, isFirstCheck: Bool, uid: String, appID: String, recordType: String) {
guard let theCursor = cursor else { return }
let operation = CKQueryOperation(cursor: theCursor)
// happens each time a record is received
operation.recordFetchedBlock = { [recordType] record in
if self.recordTypeToRecordListDict[recordType] == nil {
self.recordTypeToRecordListDict[recordType] = [record]
}
else {
self.recordTypeToRecordListDict[recordType]?.append(record)
}
}
// happens when all records are done for that cursor
operation.queryCompletionBlock = { [recordType] cursor, error in
if error == nil {
if cursor == nil { // cursor is nil => we've gotten all records, so save them
self.saveRecordCounts(records: self.recordTypeToRecordListDict[recordType]!, uid: uid, appID: appID, recordType: recordType, isFirstCheck: isFirstCheck) // use isFirstCheck, not the value in the dictionary
}
else if self.recordTypeToRecordListDict[recordType] != nil {
self.queryRecordsWithCursor(cursor: cursor, isFirstCheck: isFirstCheck, uid: uid, appID: appID, recordType: recordType) // recursive call. if we've gotten here, there's definitely a non-nil cursor
}
}
else {
self.recordTypeErrorHandling(error: error as! CKError, uid: uid, appID: appID, recordType: recordType)
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
Please let me know if you need more of my code than this! Thank you!
P.S. For what it's worth, when I step through with the debugger, setting the results limit to 5 had no effect. Not sure if that's a related issue.
Figured it out. It was a combination of issues. One was that I didn't have a recordFetchedBlock for first set of records. Another issue was that I had a for-loop that needed a DispatchGroup to synchronize things.
Another important thing I learned - If you query CloudKit records in the dashboard, it's not necessarily the case that ALL of your records show up.

Powering Master-Detail with CloudKit

I have a UISearchResultsController that searches locally while typing, and remotely (CloudKit) when the search button is pressed. The number of results returned needs to be 10-30 (I'm currently testing my app with 25 and it's always enough)
The search results list is populated with RecordType1, while it's detail is populated by RecordType1 and RecordType2. My question is how to go about fetching the second reference type while minimizing my requests/sec. I was looking at Apple's CloudCaptions sample, and they solve the problem by fetching the second record type when each of RecordType1 is fetched. It seems that this needlessly creates fetch requests (1[RecordType1] + 25[RecordType2] = 26 requests). How can I reduce this? It seems like it should be possible in two requests (one for RecordType1 and then one to fetch all the RecordType2 associated with it).
UPDATE: RecordType2 has a back reference to RecordType1
Unless I am misunderstanding your problem, I think you can just execute a query on your CloudKit database:
let searchKey = ... // value for recordType1
let z = CKRecordZone.default()
let predicate = NSPredicate(format: "recordType1 == %#", searchKey)
let query = CKQuery(recordType: "recordType2", predicate: predicate)
db.perform(query, inZoneWith: z.zoneID) { (records, error) in
if error != nil {
// `records` contains recordType2
} else {
// check for errors
}
}
You can also search for a multiple keys using the IN comparison in the predicate:
let searchKeys = ... // [value1, value2, value3, etc.]
let predicate = NSPredicate(format: "recordType1 IN %#", searchKeys)
References
CloudKit CKQueryOperation (Apple)
CloudKit CKQuery (Apple)

Resources