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)
Related
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.
I have some CKRecords in Cloudkit, the records has two property of int type, which may be called 'cost' and 'price', I want to query records whose 'cost' > 'price', but the application crashed when I write a query like this:
CKQuery *query = [[CKQuery alloc] initWithRecordType:#"MyRecordType" predicate:
[NSPredicate predicateWithFormat:#"cost > price"]];
this is the crash info given by Xcode:
Terminating app due to uncaught exception 'CKException', reason: 'Invalid predicate, Invalid right expression, is not a function expression
Please Help, thanks in advance.
OK,
Thought some more about this.
You query all records, but select only two fields; the cost and the price.
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Whatever", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["cost","price"]
Then you do your query [which should be quicker], sort out the records you want and then go get them with publicDB.fetchRecordWithID.
Yes, I know more than one query, in fact it looks like a lot more, but wait I think there is an answers in WWDC 2015 Tips & Tricks in Cloudkit; Watch the session and you might find something in there too. Here the code to fetch a record, if you have the record ID.
publicDB.fetchRecordWithID(), completionHandler: {record, error in
if error != nil {
println("there was an error \(error)")
} else {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.plasticOne.text = (record.objectForKey("subCategory") as String)
}
}
})
Sorry I cannot give you more than that without writing the code myself, and if I do that than you end up using my answer, and I sure you're answer is going to be better :)
When I fetch for records with fields that are Strings (or others) it takes under 1 second:
let query = CKQuery(recordType: "Messages", predicate: NSPredicate(format: "TRUEPREDICATE"))
NSLog("Started fetching")
self.publicDb.performQuery(query, inZoneWithID: nil) { (records, error) in
NSLog("Finished fetching")
print(records!.count)
}
But, when this record type contains a CKAsset, the time for the query goes up to at least 3 seconds. This is not acceptable when I want to load an image that is 100kb. It's the same when I put the asset in as a reference and load it from there. What can I do to speed up queries for records that contain assets or is there any other way to store assets in a more efficient way?
Use an operation and set the quality of service:
CKQueryOperation *queryOperation = [[CKQueryOperation alloc] initWithQuery:query];
queryOperation.qualityOfService = NSQualityOfServiceUserInitiated;
If you don't set qualityOfService, you will get the background level and it will be slow.
I am having an issue with core data's predicate method, in that it seems to be somewhat inconsistent in the results it is returning. I have a "Users" entity that is populated from a web API, rather than deleting all the entries then downloading and adding the users, I prefer to set a flag for all users "isModified" to false, then download the users over the API and update or add users depending on whether they are in the store or not, setting the "isModified" flag when they are added/updated. Finally the code deletes any users that did not have their "isModified" flag set. I do it this way because in the event that the web API is not available I don't lose all my users and the app can continue to work.
The problem I am having is that the method that deletes users that haven't had their "isModified" flag set is deleting users that HAVE been updated!
Here's the method:
func deleteUsersNotUpdated() -> Bool {
// default result
var result = false
// setup the fetch request
let fetchRequest = NSFetchRequest(entityName: "Users")
// set a predicate that filters only records with updated set to false
fetchRequest.predicate = NSPredicate(format: "isModified == %#", false)
do {
let fetchResults = try self.managedObjectContext.executeFetchRequest(fetchRequest) as! [NSManagedObject]
for user in fetchResults {
print("Deleting \(user)")
self.managedObjectContext.deleteObject(user)
try self.managedObjectContext.save()
}
result = true
} catch let error as NSError {
print("\(error)")
}
return result
}
The method mostly works, but intermittently will delete a user for no good reason, i.e. even though it has the "isModified" flag set, e.g. here is the output of the line: print("Deleting (user)")
Deleting <NSManagedObject: 0x7b6d6ec0> (entity: Users; id: 0x7b64abd0 <x-coredata://1A7606EB-3539-4E85-BE1C-15C722AD7125/Users/p14> ; data: {
created = "2016-01-17 16:54:21 +0000";
familyName = Doe;
givenName = John;
isModified = 1;
pin = 3932;
type = STAFF;
})
As you can see, the "isModified" flag is very definitely set, yet the predicate is supposed to select only records that have the flag reset (false).
The method above is part of a class I have created, it's basically a CRUD class for the Users entity.
Scratching my head here!
I can supply the rest of the code if required.
I think your code looks perfectly fine (although you should save outside the loop). Swift booleans are automatically converted to NSNumber and back in most situations. There are many valid ways to write this predicate.
The only possible explanation that comes to mind is that another class is using the same managed object context to modify the fetched objects. If you have a single context app this a conceivable scenario.
The standard way to avoid this is to make the changes on a background context.
Use an boolean literal rather than a object placeholder %# for a boolean value
NSPredicate(format: "isModified == FALSE")
and don't call context.save() in each iteration of the repeat loop, call it once after the repeat loop.
I'm working on Joke app that uses CloudKit
Each joke has a reference list to some categories/tags.
I'm trying to query all jokes that has some specific tags. For instance I want to find all jokes that is in the categories of Animal and Doctor.
Right now I have tried with the following code
let tagRecords = tags.map { CKReference(record: $0.record, action: .None) }
let predicate = NSPredicate(format: "tags CONTAINS %#", tagRecords)
let query = CKQuery(recordType: "Jokes", predicate: predicate)
Basically what the above does is first of all creating an array of references and then make a predicate to find the tags the contains those references
Unfortunately this doesn't work
I get the following error
server message = "Internal server error""
So the question is: How do you find all records that contains all references in a reference list?
Assuming that tags is a relationship or an array of strings, you could try with the following predicate:
let predicate = NSPredicate(format: "ANY tags IN %#", tagRecords)
Hope this helps.