Deleting a CKRecord is really confusing - ios

So I'm having a really weird problem, I suppose I either dont understand how CloudKit works under the hood or I encountered a bug in CloudKit.
So, the issue looks like this:
App initial state:
I have 5 "Package" records, lets call them A, B, C, D, E.
User action
The user will delete "Package" record E and at some later point in time he will press a refresh button which will fetch all current "Package" records from the cloud.
The problem
When the user presses the refresh button, the app will basically look at the existing locally stored "Package" records, and will create a CKQuery with a predicate that should fetch any other records that do not exist locally. The next step is basically calling the [database performQuery: inZoneWithID:completionHandler:] method.
The surprise shows up when I get the results, which contain the "Package" record E that the user previously deleted.
This doesnt seem to be right to me...
The steps I took to debug:
Right after deleting the "Package" record E, I created a CKFetchRecordsOperation and tried to fetch the deleted record. The result was as expected: I got a "Record not found". I'm cool here.
Thinking there might be some delays on the server side, I put a dispatch_after block and launched the same fetch operation I did in point 1 but just after 30 seconds. The result was still as expected: I got the "Record not found" error.
Performed the same test as I did in point 2 but with a delay of 100 seconds and ... surprise, the CKFetchRecordsOperation operation returned the deleted record E package. The weird thing is that somethings it will still return an error, but sometimes will just plainly return the deleted object.
And now the really weird part: This does not happen with record A, B, C and D, the single difference between all theses records are their names. This does not make any sense.
I filled a bug report and the reply I got was this:
This is correct behavior. Queries are eventually consistent so the deletes may not immediately be reflected when querying. Fetching the deleted record by ID via a CKFetchRecordsOperation should return a CKErrorUnknownItem right away.
While this is partially true, this does not seems to be the case with what I'm seeing.
Code
Deleting the record E with name DS2000330803AS, the check CKFetchRecordsOperation operation returns an error with Record not found. All good here.
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDB = [container privateCloudDatabase];
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName: #"DS2000330803AS"];
CKModifyRecordsOperation *operation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave: nil recordIDsToDelete: #[recordID]];
operation.database = privateDB;
[operation setModifyRecordsCompletionBlock:^(NSArray<CKRecord *> * _Nullable savedRecords,
NSArray<CKRecordID *> * _Nullable deletedRecordIDs,
NSError * _Nullable error) {
CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs:#[recordID]];
fetchOperation.database = privateDB;
[fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){
NSLog(#"Error: %#", error.localizedDescription);
}];
}];
Placing a NSTimer in my VC just to test the Record deletion, this piece of code will return the deleted record:
[NSTimer scheduledTimerWithTimeInterval:100 repeats:NO block:^(NSTimer * _Nonnull timer) {
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDB = [container privateCloudDatabase];
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:#"DS2000330803AS"];
CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs: #[recordID]];
fetchOperation.database = privateDB;
[fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){
NSLog(#"Error: %#", error.localizedDescription);
}];
[privateDB addOperation: fetchOperation];
}];
The piece of code that fetches all the existing records by pressing a refresh button which the user can press at any time. I simplified this code a bit to just expose the problem, basically the performQuery returns the DS2000330803AS record, and for the sake of testing my sanity, I'm adding a CKFetchRecordsOperation to fetch the record again, which of course does return it without any issues.
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDB = [container privateCloudDatabase];
NSPredicate *predicate = [NSPredicate predicateWithValue: YES];
CKQuery *query = [[CKQuery alloc] initWithRecordType:#"Package" predicate:predicate];
[privateDB performQuery:query
inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
[results enumerateObjectsUsingBlock:^(CKRecord * _Nonnull record, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(#"Record ID: %#", record.recordID);
CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs: #[record.recordID]];
fetchOperation.database = privateDB;
[fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){
NSLog(#"Error: %#", error.localizedDescription);
}];
[privateDB addOperation: fetchOperation];
}];
}];
Other notes: I removed and commented pretty much everything related to CloudKit and the above code is the only one that interacts with CloudKit. I'm testing with a single device at the moment.
I know the CKQuery can have a better NSPredicate, but now I try to understand why I have this issue.
P.s. When I added the first implementation of CloudKit to my app, I tried to keep it as simple as possible, without any fancy syncing stuff. It worked just fine for a year, then I started getting reports from my users that they cannot delete some records in production.
Any hints guys on how I should further debug this?
Thank you!

I think you are mixing up record Type and record Name(String of CKRecordID). Name is assigned by CloudKit(Typically) and type is set by you. I would bet it was auto assigned but I would have to see how the record was saved. It would be telling to see a screenshot of your CloudKit Dashboard.
In your block of code in
1) you are trying to delete the record name of some record using the record type. That is why you get the error "Record not found"
2) Same as you are still using Record Type and not record name
3) Fetches the record because it is actually using the assigned record.recordID.
This is my gut on the situation. As far as deleting and refreshing please see my answer on stitching records to keep UI and database in sync.

Related

Continuing CloudKit fetch in background

My goal is to be able to save a record using CloudKit when my app gets backgrounded. I first fetch the record I'm trying to modify, modify it, then save it. I'm making this call from my app delegate's applicationDidEnterBackground:
CKDatabase *db = [[CKContainer defaultContainer] privateCloudDatabase];
CKRecordID *recordId = [[CKRecordID alloc] initWithRecordName:#"record_a"];
[db fetchRecordWithID:recordId completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
// Doesn't get here until I foreground the app
if (!record) // Doesn't exist yet so create it
{
record = [[CKRecord alloc] initWithRecordType:#"SaveData" recordID:recordId];
}
NSURL *fileURL = [NSURL fileURLWithPath:#"path/to/file"];
CKAsset *saveAsset = [[CKAsset alloc] initWithFileURL:fileURL];
record[#"saveAsset"] = saveAsset;
[db saveRecord:record completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable saveError) {
// Doesn't get here until I foreground
}];
}];
The completion block for the fetchRecordWithID call doesn't get fired until I foreground the app again, meaning the saveRecord: call doesn't get called until then either.
I've tried using the CKOperation version of fetch and save with modified qualityOfService and queuePriority properties to no avail. Also tried setting the longLived property, but that didn't work either. I've scoured the docs and couldn't find anything that should stop the block from being called when the app is backgrounded.
I have other code that can call through their completion blocks when in the background, so not sure why this would be limited. Any ideas?

How do i delete cksubscription in Objective-c?

I'm sorry, but I can't find the answer in plain English, or at least every answer I see assumes I have a certain amount of knowledge that must not have. I just need to delete a CKSubscription. How do I do this? All tips and help will be greatly appreciated.
Assuming you don't have the subscriptionID of the subscription you want to delete, you should first fetch all of the subscriptions and figure out the subscriptionID of the subscription you want to delete:
[[[CKContainer defaultContainer] publicCloudDatabase] fetchAllSubscriptionsWithCompletionHandler:^(NSArray<CKSubscription *> * _Nullable subscriptions, NSError * _Nullable error) {
if (!error) {
//do your logic to find out which subscription you want to delete, and get it's subscriptionID
} else {
//handle error
}
}];
Then, now that you have the subscriptionID, simply delete it:
CKModifySubscriptionsOperation *operation = [[CKModifySubscriptionsOperation alloc] initWithSubscriptionsToSave:nil subscriptionIDsToDelete:YOUR_SUBSCRIPTION_ID];
operation.modifySubscriptionsCompletionBlock = ^(NSArray <CKSubscription *> * __nullable savedSubscriptions, NSArray <NSString *> * __nullable deletedSubscriptionIDs, NSError * __nullable operationError) {
//handle the results when the operation has completed
};
[[[CKContainer defaultContainer] publicCloudDatabase] addOperation:operation];
Don't forget to always handle all the possible errors properly.

Parse.com iOS objectWithoutDataWithObjectId and Local Datastore

Context: I would like my game app to work completely offline so users don't need an internet connection to start playing (certain features will be disabled without connectivity). To this end I've exported a PFObject subclass of static data from parse.com that I'd like to include in my app bundle so users don't need to download it from parse.com.
Say my PFObject subclass of static data is Foo, and I have another PFObject subclass called Bar with a pointer to Foo. I run all the following in Airplane Mode:
Foo *foo = [Foo objectWithoutDataWithObjectId:#"someObjectIdFromServer"];
foo.someKey = #"someValue";
Bar *bar = [Bar object];
bar.foo = foo;
[bar pinInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded && !error) {
// Now I query from the local datastore:
PFQuery *query = [Bar query];
[query includeKey:#"foo"];
[query fromLocalDatastore];
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if (!objects || error) {
// error
} else {
Bar *bar = [objects firstObject];
NSLog(#"bar.foo: %#", bar.foo); // this prints the object out fine
NSLog(#"bar.foo.isDataAvailable: %d", bar.foo.isDataAvailable); // this is 0, even though I called [query includeKey:#"foo"];
NSLog(#"bar.foo.someKey: %#", bar.foo.someKey); // this silently doesn't return. No exception raised or nil value, execution just stops here
NSLog(#"more logging"); // this doesn't print but program execution continues
[bar.foo fetchFromLocalDatastoreInBackground]; // trying this just in case
NSLog(#"bar.foo.isDataAvailable: %d", bar.foo.isDataAvailable); // still 0 even with fetch
}
}];
}
}];
Any idea why bar.foo.isDataAvailable is 0, even though I called [query includeKey:#"foo"] and even fetch? My understanding is that objectWithoutDataWithObjectId can be used to create local copies of objects in the parse cloud that can then be used in pointer relationships. Am I misunderstanding what can be done with these objects when only using the local datastore (i.e. in airplane mode)?
Also I've tried the same thing without using objectWithoutDataWithObjectId (instead doing [Foo object]) and that works fine. I can potentially use this in a workaround (along with some Cloud Code to ensure uniqueness on my static data), but I'd prefer not to since that counts towards my Cloud Data quota.
Any feedback is very appreciated!

Accessing Health Kit data into Apple Watch OS 2 excluding Workout Data

I am able to access Workout data using workout session but unable to do the same with others such as accessing Height,Weight,Dietary Water, Body Temperature,Blood Pressure etc.
Also i am able to access heart rate but unable to access body temp. Both of them are same vital sign identifiers.
Is it that watch can access only Workout data as mentioned in WWDC 2015 video?
Sample Code:
-(void)bodyTempForLabel :(WKInterfaceLabel *)bodyTempLabel {
HKSampleType *bodyTemp = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierBodyTemperature];
[self readMostRecentSampleType:bodyTemp withCompletion:^(HKQuantitySample *quantitySample, NSError *error) {
if(error) {
NSLog(#"Error Reading Weight From health Kit");
}
self.bodyTemp = quantitySample;
double bodyTempinDegree = [[self.bodyTemp quantity] doubleValueForUnit:[HKUnit unitFromString:[NSString stringWithFormat:#"%#C", #"\u00B0"]]];
dispatch_async(dispatch_get_main_queue(), ^{
[bodyTempLabel setText:[NSString stringWithFormat:#"%f",bodyTempinDegree]];
});
}];
}
-(void)readMostRecentSampleType : (HKSampleType *)sampleType withCompletion:(void(^)(HKQuantitySample *quantitySample,NSError *error))recentSample {
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:HKSampleSortIdentifierEndDate ascending:NO];
HKQuery *sampleQuery = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:nil limit:HKObjectQueryNoLimit sortDescriptors:#[sortDescriptor] resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
if(!error) {
// No results retuned array empty
HKQuantitySample *mostRecentSample = results.firstObject;
recentSample(mostRecentSample,error);
}
}];
[_healthStore executeQuery:sampleQuery];
}
Any help would be appreciated. Thanks!!!
It seems you'll need to use a real device to debug. I'm unable to get any value from HK when running the simulator but it works fine in the Apple Watch. (Using XCode 7 Beta 5).
The apple watch has access to all health kit types (though only a subset of the data). Has your app asked for permission for all of those types? Each type you want to read or write needs to be explicitly asked for when you setup your health store. For example, to read energy burned, distance, and heart rate you need to include:
let typesToRead = Set([
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)!
])
self.healthStore.requestAuthorizationToShareTypes(typesToShare, readTypes: typesToRead) { success, error in
// ...
}

Modify data with CloudKit

I have data I'd like to modify in CloudKit. I've found this question (Saving Modified Data in CloudKit) and it points to CKModifyRecordsOperation, but being new to this I'm looking for more guidance. I'm setting my object like so:
[object setValue:number forKey:#"total"];
If I'm only modifying one record and not all do I still call CKModifyRecordsOperation?
Any clues to how this is done?
I've been using [self.cloudManager saveRecord:object]; but with modifying the record this isn't working.
You can fetch, modify, and save changes you make to individual records.
The code snippet below shows how to fetch an Artwork record, changes the date attribute value, and saves it to the database.
// Fetch the record from the database
CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:containerIdentifier] publicCloudDatabase];
CKRecordID *artworkRecordID = [[CKRecordID alloc] initWithRecordName:#"115"];
[publicDatabase fetchRecordWithID:artworkRecordID completionHandler:^(CKRecord *artworkRecord, NSError *error) {
if (error) {
// Error handling for failed fetch from public database
}
else {
// Modify the record and save it to the database
NSDate *date = artworkRecord[#"date"];
artworkRecord[#"date"]; = [date dateByAddingTimeInterval:30.0 * 60.0];
[publicDatabase saveRecord:artworkRecord completionHandler:^(CKRecord *savedRecord, NSError *saveError) {
// Error handling for failed save to public database
}];
}
}];
Consider to read this article for more detailed information.

Resources