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.
Related
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.
I want to handle the error case where endTurnWithNextParticipants fails. I therefore store match data by calling the cacheFailedEndTurnWithNextParticipants method at the end of this post. I provided the function only for reference.
The problem is that I don't know how I shall restore the data in the uncache method. How can I load the GKTurnBasedPlayers for the nextParticipants list? I use GKPlayer::loadPlayersForIdentifiers now, but I get the following warning:
"/Users/eternity/Documents/Xcode/DominationSK/DominationSK/GKMatchCache.m:134:74: Incompatible pointer types sending 'NSArray<GKPlayer *> * _Nullable' to parameter of type 'NSArray<GKTurnBasedParticipant *> * _Nonnull'"
How can I load GKTurnBasedParticipants from ID's?
Shall this retransmitting be done in another way?
-(void)uncache
{
if (_cache.count > 0)
{
NSDictionary *d = _cache[0];
[_cache removeObjectAtIndex:0];
[GKTurnBasedMatch loadMatchWithID:d[MATCH_ID]
withCompletionHandler:^(GKTurnBasedMatch * _Nullable match, NSError * _Nullable error) {
if (error == nil)
{
bad load method --> [GKPlayer loadPlayersForIdentifiers:d[PARTICIPANTS]
withCompletionHandler:^(NSArray<GKPlayer *> * _Nullable nextParticipants, NSError * _Nullable error) {
if (error == nil)
{
NSNumber *timeout = d[TIMEOUT];
warning --> [match endTurnWithNextParticipants:nextParticipants
turnTimeout:timeout.longValue
matchData:d[DATA]
completionHandler:^(NSError * _Nullable error){
// This is not finished
}];
}
else
{
[self addToCache:d]; // TODO: Add time to live
}
}];
}
else
{
[self addToCache:d]; // TODO: Add time to live
}
}];
}
}
// Just for reference
-(void)cacheFailedEndTurnWithNextParticipants:(GKTurnBasedMatch*)match
participants:(NSArray<GKTurnBasedParticipant*>* _Nonnull)nextParticipants
turnTimeout:(NSTimeInterval)timeout
matchData:(NSData* _Nonnull)matchData
error:(NSError* _Nonnull)error
{
if ([self shallBeCached:error])
{
NSMutableArray *playerIds = [[NSMutableArray alloc] init];
for (GKTurnBasedParticipant *p in nextParticipants)
{
[playerIds addObject:p.player.playerID];
}
NSDictionary *d = #{MATCH_ID : match.matchID,
PARTICIPANTS : playerIds,
TIMEOUT : [NSNumber numberWithLong:timeout],
DATA : matchData};
[self addToCache:d];
}
}
It's possible to get the participants from the match, and then retrieve player IDs from that array, and finally update the participants attribute in the match.
There are multiple places within my app that I display a list of objects returned from the server. I'm trying to re-write some code so it only has to be modified in one place, rather than three, for updates.
The basic gist looks like this:
PFQuery *query = [PFQuery queryWithClassName:"MyClass"];
//query constraints
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if( !error ) //Do stuff with objects;
else [self displayError:error];
}];
I have subclassed PFObject for this class, so I have a class called MyClass. I was trying to put a class method on this class that returns an NSArray * containing the results of a query. However, the following function throws some errors:
+(NSArray *)getListOfMyClass {
PFQuery *query = [PFQuery queryWithClassName:"MyClass"];
//query constraints
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if( !error ) return objects;
else [self displayError:error];
}];
}
This error appears at the end of the function:
Control may reach end of non-void block
This error appears at return objects;: Incompatible block pointer types sending 'NSArray * _Nullable (^)(NSArray * _Nullable __strong, NSError * _Nullable __strong)' to parameter of type 'PFQueryArrayResultBlock _Nullable' (aka 'void (^)(NSArray<PFGenericObject> * _Nullable __strong, NSError * _Nullable __strong)')
So, I know this issue is due to the asynchronous behavior of this query. I need to call this query from a few different places, so was hoping I could call the single function, but now I'm lost as to how make this efficient and not very mucked up. I'm sorta leaning towards just doing it in line three times, as that's what I was doing already anyway, but I'm trying to learn better practices.
Is there a way I can force this function to wait to return until the query has succeeded? Or would it be OK to pass in sender, and then trigger a function on the sender that passes in the results? I'm gonna be working on the latter to see if it works, but even that seems so roundabout. I'm wondering if there's something obvious I'm missing.
Thanks for your help!
Note - I would prefer Objective C answers, but can work with a Swift answer if need be.
According to PFQuery.h :
typedef void (^PFQueryArrayResultBlock)(NSArray<PFGenericObject> *_Nullable objects, NSError * _Nullable error);
The findObjectsInBackgroundWithBlock is a void block which returns nothing, so obviously the return statement gonna cause error.
If you want to retrieve the array, you can use a completion handler, like:
+(void)getListOfMyClassWithCompletionBlock:(void (^)(NSError *error, NSArray *objectArray))block {
PFQuery *query = [PFQuery queryWithClassName:"MyClass"];
//query constraints
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if( !error ) block(nil, objects);
else block(error, nil); [self displayError:error];
}];
}
And somewhere else (other classes) in your code:
[YourClass getListOfMyClassWithCompletionBlock:^(NSError *error, NSArray *objectArray) {
if (!error) //update your UI with objectArray
}];
I'm trying to incorporate GKGameSession into my Game Center game. I've tried several combinations of the following code: running the commands asynchronously, chaining them in the completion handlers, etc. Every time I see the same result: I can use saveData just fine until I've called getShareURLWithCompletionHandler. After that, any attempt to saveData throws an error.
Here's the simplest version of code that exhibits the problem:
CKContainer *defaultContainer = [CKContainer defaultContainer];
[GKGameSession createSessionInContainer:defaultContainer.containerIdentifier
withTitle:#"temp title"
maxConnectedPlayers:4
completionHandler:^(GKGameSession * _Nullable session, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
[session getShareURLWithCompletionHandler:^(NSURL * _Nullable url, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
NSData *newData = [NSData dataWithBytesNoCopy:#"abcdefghijklmnopqrstuvwxyz" length:26];
[reSession saveData:newData completionHandler:^(NSData * _Nullable conflictingData, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
}];
In most cases, the saveData call simply crashes:
malloc: *** error for object 0x32df14: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
But sometimes it throws an error:
GKGameSessionErrorDomain:GKGameSessionErrorUnknown
I've tried different kinds of data being saved. I've tried making the calls sequential by chaining all the calls in completion handlers. I've tried doing the URL fetch and data save inside and outside of the creationSession completion handler.
Is there something I'm doing wrong here?
I see the same, but with a more useful error:
The requested operation could not be completed because the session has been updated on the server, causing a conflict.
The save documentation says,
It is up to the developer to decide how to handle save conflicts.
Here though, retrying the save fails every time, forever. So yeah, that's the same state you're in.
However, when the player joining the game enters the URL on their device, their GKGameSessionEventListener's didAddPlayer: is called, and then if they save... they get the same conflict error, but if they then retry the save... it works!
The player creating the link is locked out of saving or updating game state, until joining players have updated the data. When the other player saves, the original player gets a call to session:player:didSave: on the GKGameSessionEventListener.
At that point the original player can then save as expected.
You should put one block inside other. Because blocks may be completed in any order.
I have working code like this:
NSData *newData = [NSData dataWithBytesNoCopy:#"abcdefghijklmnopqrstuvwxyz" length:26];
[reSession saveData:newData completionHandler:^(NSData * _Nullable conflictingData, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
} else {
[session getShareURLWithCompletionHandler:^(NSURL * _Nullable url, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
}
}];
CKContainer *c= [CKContainer defaultContainer];
CKDatabase *openbase=c.publicCloudDatabase;
CKDatabase *privateDatabase=c.privateCloudDatabase;
NSPredicate *predicate=[NSPredicate predicateWithValue:YES];
CKQuery *q=[[CKQuery alloc]initWithRecordType:#"Notesss" predicate:predicate];
[openbase performQuery:q inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
if (error!=nil) {
NSLog(#"%#",error);
} else {
[results enumerateObjectsUsingBlock:^(CKRecord * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *title= [obj valueForKey:#"title"];
if( [title containsString:[NSString stringWithFormat:#"%ld",(long)row+1]]){
CKAsset *imageA=[obj valueForKey:#"dataImage"];
dispatch_sync(dispatch_get_main_queue(), ^{
self.imageV.image=[UIImage imageWithContentsOfFile:imageA.fileURL.path];
});
};
}];
}}];
Is it current to request from iCloud?
Can I use SDWebImage when I request from iCloud?
What do you mean? CloudKit is based on iCloud, but it's not the same. What do you want to improve. I don't understand your questions completely.
In your code I see you already get the image from the CloudKit record / CKAsset. What else do you want to do with it?. The moment you have the CKAsset the image is just a file on your phone. You can do whatever you want with it.