I keep seeing this error when performing a query in CloudKit. Here is the code that usually sets it off:
if (self.userID) {
CKReference *referenceToUser = [[CKReference alloc]initWithRecordID:self.userID action:CKReferenceActionDeleteSelf];
NSPredicate *userSearch = [NSPredicate predicateWithFormat:#"%K == %#", #"User", referenceToUser];
CKQuery *findHoneymoon = [[CKQuery alloc]initWithRecordType:#"Honeymoon" predicate:userSearch];
CKQueryOperation *findHMOp = [[CKQueryOperation alloc]initWithQuery:findHoneymoon];
findHMOp.resultsLimit = 1;
__block CKRecord *userHoneyMoon;
findHMOp.recordFetchedBlock = ^(CKRecord *record){
userHoneyMoon = record;
self.userHoneymoon.honeymoonID = record.recordID;
};
__block BOOL errorOccured = NO;
findHMOp.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *operationError){
if (operationError)
{
NSLog(#"Error searching for user honeymoon, description: %#, and code: %lu, and heck, heres the domain: %#", operationError.localizedDescription, operationError.code, operationError.domain);
[[NSNotificationCenter defaultCenter]postNotificationName:#"HoneymoonError" object:nil];
errorOccured = YES;
}
if (!userHoneyMoon && !errorOccured)
{
[self createBlankHoneymoon];
}
else
{
[self.userHoneymoon populateHoneymoonImages];
}
};
[[[CKContainer defaultContainer] publicCloudDatabase] addOperation:findHMOp];
}
What's really vexing is that this error only occurs about 1 in 5 times I try to run the program.
Here is what gets logged when the error occurs:
Error searching for user honeymoon, description: No operations present in request, and code: 12, and heck, heres the domain: CKErrorDomain
As you can see from the code there ARE operations in the request, does anyone know any other reason this error might occur and how to handle it?
Related
and thank you for your advice!
I am using fetchAllSubscriptionsWithCompletionHandler, and I do see the subscritionID from each CKSubscription after the push notification is sent. How do I retrieve the CKRecord from the subscriptionID?
I do not see the Remote Push Notification from a CKReference that was created. I can see the CKRecord and its related record via CloudKit DashBoard. I do receive a Push Notification from when its parent record is created, but not when the CKReference is created on the child record.
-(void)SubscribeForReference:(CKRecord *)record
{
NSUserDefaults *trackSubscription = [NSUserDefaults standardUserDefaults];
BOOL hasSubscribed = [[NSUserDefaults standardUserDefaults] objectForKey:#"alreadySubscribedForReference"] != nil;
//BOOL hasSubscribed = [trackSubscription objectForKey:#"alreadySubscribedForReference"];
if (hasSubscribed == false) {
//creates a subscription based on a CKReference between two ckrecords
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"sentences == %#", record.recordID];
// 1) subscribe to record creations
CKSubscription *subscriptionRelation =
[[CKSubscription alloc] initWithRecordType:#"RecordTypeName"
predicate:predicate
options:CKSubscriptionOptionsFiresOnRecordCreation | CKSubscriptionOptionsFiresOnRecordUpdate | CKSubscriptionOptionsFiresOnRecordDeletion | CKSubscriptionOptionsFiresOnRecordUpdate];
//http://stackoverflow.com/questions/27371588/cloudkit-notifications
CKNotificationInfo *notificationInfo = [[CKNotificationInfo alloc] init];
// I added this because of apple's documentation
notificationInfo.desiredKeys = #[#"word",#"identifier"];
notificationInfo.alertLocalizationArgs = #[#"word"];
notificationInfo.alertLocalizationKey = #"%1$#";
notificationInfo.shouldBadge = YES;
subscriptionRelation.notificationInfo = notificationInfo;
[self.privateDatabase saveSubscription:subscriptionRelation completionHandler:^(CKSubscription * _Nullable subscription, NSError * _Nullable error) {
if (error == nil) {
[trackSubscription setObject:subscription.subscriptionID forKey:#"alreadySubscribedForReference"];
[trackSubscription synchronize];
}else
NSLog(#"something went wrong with saving the CKSubcription with error...\n%#\n",[error localizedDescription]);
}];
}
else{
NSLog(#"\nSubscribeForReference: ALREADY has subscription: %# set for key 'alreadySubscribedForReference' \n\n ", [trackSubscription objectForKey:#"alreadySubscribedForReference"]);
}
}
The code below is ran when the app is launched, provided there is an Internet connection:
-(void)runWhenAppStarts
{
CKFetchSubscriptionsOperation *fetchSubscriptionsOperation = [CKFetchSubscriptionsOperation fetchAllSubscriptionsOperation];
fetchSubscriptionsOperation.fetchSubscriptionCompletionBlock = ^(NSDictionary *subscriptionsBySubscriptionID, NSError *operationError) {
if (operationError != nil)
{
// error in fetching our subscription
CloudKitErrorLog(__LINE__, NSStringFromSelector(_cmd), operationError);
if (operationError.code == CKErrorNotAuthenticated)
{
// try again after 3 seconds if we don't have a retry hint
//
NSNumber *retryAfter = operationError.userInfo[CKErrorRetryAfterKey] ? : #3;
NSLog(#"Error: %#. Recoverable, retry after %# seconds", [operationError description], retryAfter);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryAfter.intValue * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self subscribe];
});
}
}
else
{
if (self.subscribed == NO)
{
// our user defaults says we haven't subscribed yet
//
if (subscriptionsBySubscriptionID != nil && subscriptionsBySubscriptionID.count > 0)
{
// we already have our one CKSubscription registered with the server that we didn't know about
// (not kept track in our NSUserDefaults) from a past app install perhaps,
//
NSLog(#"\nsubscriptionsBySubscriptionID (dictionary) = %#\n",subscriptionsBySubscriptionID);
NSArray *allSubscriptionIDKeys = [subscriptionsBySubscriptionID allKeys];
NSLog(#"\nallSubscriptionIDKeys (array) = %#\n",allSubscriptionIDKeys);
if (allSubscriptionIDKeys != nil)
{
[self updateUserDefaults:allSubscriptionIDKeys[0]];
for (NSString *subscriptions in allSubscriptionIDKeys) {
NSLog(#"subscriptionID: %#\n",subscriptions);
}
}
}
else
{
// no subscriptions found on the server, so subscribe
NSLog(#"...starting subscriptions on server...\n");
[self startSubscriptions];
}
}
else
{
// our user defaults says we have already subscribed, so check if the subscription ID matches ours
//
NSLog(#"...our user defaults says we have already subscribed, with subscriptionsBySubscriptionID = %#\nso check if the subscription ID matches the one already stored in NSUserDefaults...\n",subscriptionsBySubscriptionID);
if (subscriptionsBySubscriptionID != nil && subscriptionsBySubscriptionID.count > 0)
{
// we already have our one CKSubscription registered with the server that
// we didn't know about (not kept track in our NSUserDefaults) from a past app install perhaps,
//
//NSDictionary *subscriptionsBySubscriptionID has a structure of #{key: value} == #{NSString: CKSubscription}
NSArray *allSubscriptionIDKeys = [subscriptionsBySubscriptionID allKeys];//contains the NSString representation of the subscriptionID.
NSArray *allSubscriptionIDVALUES = [subscriptionsBySubscriptionID allValues];// the values are the corresponding CKSubscription objects
for (CKSubscription *values in allSubscriptionIDVALUES) {
NSLog(#"\nCKSubscriptionValue = %#\n",values);
}
NSLog(#"\n...we already have our one CKSubscription registered with the server that..so lets look at allSubscriptionIDKeys =%#.\n\nvalues ...\nallSubscriptionIDVALUES = %#\n\n",allSubscriptionIDKeys,allSubscriptionIDVALUES);
if (allSubscriptionIDKeys != nil)
{
NSString *ourSubscriptionID = [[NSUserDefaults standardUserDefaults] objectForKey:kSubscriptionIDKey];
if (![allSubscriptionIDKeys[0] isEqualToString:ourSubscriptionID])
{
// our subscription ID doesn't match what is on the server, to update our to match
NSLog(#"...our subscription ID doesn't match what is on the server, going to update our NSUserDefaults...\n");
[self updateUserDefaults:allSubscriptionIDKeys[0]];
}
else
{
// they match, no more work here
NSLog(#"...iCloud server already has this subscriptionID, so do nothing.i.e. don't subscribe again..\n");
}
}
}
}
}
};
[self.privateDatabase addOperation:fetchSubscriptionsOperation];
}
When you fetch subscriptions, you don't get a specific record ID. Instead, the subscription's .recordType will tell you the type of record this subscription monitors. Typically, if you have 1,000 users, then you would have 1,000 instances of the record, and each user would create subs to monitor when their instance is modified.
When the subscription is triggered, you'll receive a notification. You can use CKFetchNotificationChangesOperation to retrieve the notifications. Each notif includes a .recordID value that tells you specifically which record changed and caused the subscription to fire.
You're currently querying the subs to make sure the user has properly subscribed. Next, you also need to query the notifications with CKFetchNotificationChangesOperation to see which records have been updated when the subscription(s) fired.
You may extract RecordID from the predicate of subscription.
For example - if subscription predicate was:
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"(author == %#)", artistRecordID];
where artistRecordID is reference to another recordType - then you may extract RecordID in the following way:
[publicDatabase fetchAllSubscriptionsWithCompletionHandler:^(NSArray<CKSubscription *> *subscriptions, NSError *error){
if(error){
// handle error
}
else {
[subscriptions indexOfObjectPassingTest:^BOOL(CKSubscription * obj, NSUInteger idx, BOOL *stop){
if ([obj.predicate isKindOfClass:[NSComparisonPredicate class]]) {
NSComparisonPredicate *p2a = (NSComparisonPredicate *)obj.predicate;
NSExpression *e6 = p2a.rightExpression;
CKReference* ref=e6.constantValue;
// you may extract RecordID here from ref
// for example - to compare against another RecordID:
if([ref.recordID.recordName isEqualToString:artistRecordID.recordID.recordName]){
*stop=YES;
return YES;
}
}
return NO;
}];
}
}];
I set subscription notifications for cloudkit. Here is my code:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"TRUEPREDICATE"];
CKSubscription *subscription = [[CKSubscription alloc]
initWithRecordType:recordType
predicate:predicate
options:CKSubscriptionOptionsFiresOnRecordCreation];
CKNotificationInfo *notificationInfo = [CKNotificationInfo new];
notificationInfo.alertLocalizationKey =#"New record in cloudKit";
notificationInfo.shouldBadge = YES;
notificationInfo.soundName = UILocalNotificationDefaultSoundName;
notificationInfo.shouldSendContentAvailable = YES;
subscription.notificationInfo = notificationInfo;
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *publicDatabase = [container publicCloudDatabase];
[publicDatabase saveSubscription:subscription
completionHandler:^(CKSubscription *subscription, NSError *error) {
if (!error)
{
NSLog(#"no error");
}
else
{
NSLog(#"error%#", error);
}
}];
and works just fine. The problem is the badges, they seem like cloudKit doesn't reset the badge number and the keep increasing even when I set the badge count to zero.
- (void)applicationDidBecomeActive:(UIApplication *)application
{
application.applicationIconBadgeNumber = 0;
}
When the app received a new notification goes from 0 to 5 (and every new notification increase by 1, the next time would be 6)
Any of you knows how keep track of the right count of badges from cloudkit (in Objective-C )
This is a duplicate of CloudKit won't reset my badge count to 0
The answer there was:
You need to do a CKModifyBadgeOperation after processing your notifications.
Here is my Swift function which I call after marking all the notifications as read. I add the operation to the defaultContainer instead of just starting it - I wonder does that make any difference.
func resetBadgeCounter() {
let badgeResetOperation = CKModifyBadgeOperation(badgeValue: 0)
badgeResetOperation.modifyBadgeCompletionBlock = { (error) -> Void in
if error != nil {
println("Error resetting badge: \(error)")
}
else {
UIApplication.sharedApplication().applicationIconBadgeNumber = 0
}
}
CKContainer.defaultContainer().addOperation(badgeResetOperation)
}
This will help.
CKModifyBadgeOperation *badgeResetOperation = [[CKModifyBadgeOperation alloc] initWithBadgeValue:0];
[badgeResetOperation setModifyBadgeCompletionBlock:^(NSError * operationError) {
if (!operationError) {
[UIApplication sharedApplication].applicationIconBadgeNumber = 0;
}
}];
[[CKContainer defaultContainer] addOperation:badgeResetOperation];
I’m trying to make a custom matchmakingview using a matchmaker. The code below is used to find a match.
When i run this on two different devices with different Game Center accounts, both will get a match but none will connect to the match. They will just get stuck in the while loop in infinity and never get out. Have i missed something, do you need to call something to actually connect to the match?
- (void) findMatch{
GKMatchRequest *request = [[GKMatchRequest alloc] init];
request.minPlayers = 2;
request.maxPlayers = 2;
request.playersToInvite = nil;
NSLog(#"Start searching!");
[matchmaker findMatchForRequest:request
withCompletionHandler:^(GKMatch *match, NSError *error)
{
if (error) {
// Print the error
NSLog(#"%#", error.localizedDescription);
}
else if (match != nil)
{
curMatch = match;
curMatch.delegate = self;
NSLog(#"Expected: %i", match.expectedPlayerCount);
while (match.expectedPlayerCount != 0){
NSLog(#"PLayers: %i", curMatch.playerIDs.count);
}
NSLog(#"Start match!");
}
}];
You should not be using a while loop to wait for expectedPlayerCount to reach 0, instead implement the GKMatchDelegate method:
- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state {
if (!self.matchStarted && match.expectedPlayerCount == 0) {
self.matchStarted = YES;
//Now you should start your match.
}
}
I'm trying to implement a real-time multiplayer game with a custom UI (no GKMatchMakerViewController). I'm using startBrowsingForNearbyPlayersWithReachableHandler:
^(NSString *playerID, BOOL reachable) to find a local player, and then initiating a match request with the GKMatchmaker singleton (which I have already initiated).
Here's where I'm having trouble. When I send a request, the completion handler fires almost immediately, without an error, and the match it returns has an expected player count of zero. Meanwhile, the other player definitely has not responded to the request.
Relevant code:
- (void) findMatch
{
GKMatchRequest *request = [[GKMatchRequest alloc] init];
request.minPlayers = NUM_PLAYERS_PER_MATCH; //2
request.maxPlayers = NUM_PLAYERS_PER_MATCH; //2
if (nil != self.playersToInvite)
{
// we always successfully get in this if-statement
request.playersToInvite = self.playersToInvite;
request.inviteeResponseHandler = ^(NSString *playerID, GKInviteeResponse
response)
{
[self.delegate updateUIForPlayer: playerID accepted: (response ==
GKInviteeResponseAccepted)];
};
}
request.inviteMessage = #"Let's Play!";
[self.matchmaker findMatchForRequest:request
withCompletionHandler:^(GKMatch *match, NSError *error) {
if (error) {
// Print the error
NSLog(#"%#", error.localizedDescription);
}
else if (match != nil)
{
self.currentMatch = match;
self.currentMatch.delegate = self;
// All players are connected
if (match.expectedPlayerCount == 0)
{
// start match
[self startMatch];
}
[self stopLookingForPlayers];
}
}];
}
Figured it out! I needed to call - (void)matchForInvite:(GKInvite *)invite completionHandler:(void (^)(GKMatch *match, NSError *error))completionHandler in my invitation handler so that both players have the same match data.
I’m missing the boat on something that I thought I had the hang of and was hoping someone here could help.
I’m using Xcode version 4.4 and my project is using Core Data, Storyboards and ARC.
My project has the following 3 entities.
All works well with the Livestock and Notes entities. But when I try to save data to the Taxonomy entity nothing happens. I get no error but the data is not saved.
Is it ok that I not use an array for the fetched results? Based on my predicate, I’m expecting only one object to be returned so I thought I didn’t need an array. Below is my code that does the saving. I have verified that data is being passed in from the view's text variables.
AppDelegate *appDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = [appDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.entity = [NSEntityDescription entityForName:#"Livestock" inManagedObjectContext:managedObjectContext];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"tank == %# AND type == %# AND name == %#", self.detailTank, self.detailType, self.detailName];
NSError *error = nil;
Livestock *livestock = [[managedObjectContext executeFetchRequest:fetchRequest error:&error] lastObject];
if (error) {
NSLog(#"TaxonViewController: saveTaxonomy: Retrieving Livestock Record for saving: error = %#", error);
}
Taxonomy *taxonomy = livestock.taxonChildren;
taxonomy.kingdom = self.kingdom.text;
taxonomy.phylum = self.phylum.text;
taxonomy.classs = self.classs.text;
taxonomy.order = self.order.text;
taxonomy.family = self.family.text;
taxonomy.genus = self.genus.text;
taxonomy.species = self.species.text;
taxonomy.common = self.common.text;
taxonomy.livestockParent = livestock;
error = nil;
if (![managedObjectContext save:&error]) {
NSLog(#"Taxonomy save error. error = %#, userInfo = %#", error, [error userInfo]);
}
Any insight is greatly appreciated! Thanks!
UPDATE 1:
Modified code to test for NULL taxonChildren value. This solved it for me. Thanks Jesse!
if (livestock.taxonChildren == NULL) {
Taxonomy *taxonomy = [NSEntityDescription insertNewObjectForEntityForName:#"Taxonomy" inManagedObjectContext:managedObjectContext];
taxonomy.kingdom = self.kingdom.text;
taxonomy.phylum = self.phylum.text;
taxonomy.classs = self.classs.text;
taxonomy.order = self.order.text;
taxonomy.family = self.family.text;
taxonomy.genus = self.genus.text;
taxonomy.species = self.species.text;
taxonomy.common = self.common.text;
taxonomy.livestockParent = livestock;
}
else {
Taxonomy *taxonomy = livestock.taxonChildren;
taxonomy.kingdom = self.kingdom.text;
taxonomy.phylum = self.phylum.text;
taxonomy.classs = self.classs.text;
taxonomy.order = self.order.text;
taxonomy.family = self.family.text;
taxonomy.genus = self.genus.text;
taxonomy.species = self.species.text;
taxonomy.common = self.common.text;
taxonomy.livestockParent = livestock;
}
Is your livestock.taxonChildren nil? You'll have to insert an instance of that object into your context first; it won't create one automatically, even with a one-to-one relationship.
Note that you should not test for error like this:
if (error) { ... }
because error may be garbage when the fetch request is successful. You should instead test the return value:
NSArray *results = [managedObjectContext executeFetchRequest...]
if (!results) { ... }