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];
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’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 am trying to pull the turn based games in which a player is participating in order to populate my tableView.
This is my function to pull their games:
- (void) loadMatchDataWithArray:(NSMutableArray*)currentGames Flag:(bool*)returned
{
NSMutableArray* __block blockGames = currentGames;
bool* __block blockReturn = returned;
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error)
{
if (matches)
{
for (int i = 0; i < matches.count; i++)
{
[(GKTurnBasedMatch*)matches[i] loadMatchDataWithCompletionHandler: ^(NSData *matchData, NSError *error)
{
int size = [matchData length];
if (size != 0)
{
Game* game = [NSKeyedUnarchiver unarchiveObjectWithData:matchData];
[blockGames addObject:game];
}
else
{
Game* game = [[Game alloc] init];
[blockGames addObject:game];
game.activePlayer = [GKLocalPlayer localPlayer];
}
*blockReturn = true;
}];
}
}
else
{
*blockReturn = true;
}
}];
}
And this is where I call it:
- (void)viewDidLoad
{
[super viewDidLoad];
[[self tableView]
setBackgroundView:[[UIImageView alloc]
initWithImage:[UIImage imageNamed:#"iPhoneBackground-568h"]]];
bool* returned = false;
[[GKMatchHelper sharedInstance] loadMatchDataWithArray:currentGames Flag:returned];
while (!returned);
[self.tableView reloadData];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
Sadly, this is just giving me a blank black screen and never returns. Is there a way that I can detect when my block comes back and display a loading spinner until then, at which point I would reload the table?
EDIT:
I have revised my code and brought the function inside my MainMenuViewController, and now it builds but never displays the data.
- (void) loadMatchData
{
NSMutableArray* __block blockGames = currentGames;
MainMenuViewController* __weakSelf = self;
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error)
{
if (matches)
{
for (int i = 0; i < matches.count; i++)
{
[(GKTurnBasedMatch*)matches[i] loadMatchDataWithCompletionHandler: ^(NSData *matchData, NSError *error)
{
int size = [matchData length];
if (size != 0)
{
Game* game = [NSKeyedUnarchiver unarchiveObjectWithData:matchData];
[blockGames addObject:game];
}
else
{
Game* game = [[Game alloc] init];
[blockGames addObject:game];
game.activePlayer = [GKLocalPlayer localPlayer];
}
[__weakSelf.tableView reloadData];
}];
}
}
[__weakSelf.tableView reloadData];
}];
[__weakSelf.tableView reloadData];
}
And now in my ViewDidLoad I just call:
[self loadMatchData];
Oh dear. Do NOT halt the program execution with "while" loops!
Why not simply call [self.tableView reloadData] at the end of your block?
So,
Remove the last 2 lines in the viewDidLoad method
Replace *blockReturn = true; with [self.tableView reloadData] (you might need to keep a weak reference to 'self' to avoid retain cycles)
Never ever use while (this and that) to wait for an operation to complete. A non-responsive UI is bad and it will cause the users to abandon your app.
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 doing an ios app based on maths..
I've done the code for uploading the high score to the Game Center..
but this doesn't work..
it's always showing 0 as the high score..
This is my code...
[[GKLocalPlayer localPlayer]authenticateWithCompletionHandler:^(NSError *error)
{
if (error ==nil)
{
CCLOG(#"Success");
} else
{
CCLOG(#"Fail");
}
}];
.
.
.
.
.
-(void)showLeaderboard
{
if( ! gameCenterViewController_ )
gameCenterViewController_ = [[GameCenterViewController alloc] init];
[gameCenterViewController_ showLeaderboard];
}
-(void)submitMyScore1:(int)score1
{
CCLOG(#"submitMyScore1--%d",score1);
//This is the same category id you set in your itunes connect GameCenter LeaderBoard
GKScore *myScoreValue = [[[GKScore alloc] initWithCategory:#"bigwizlist"] autorelease];
myScoreValue.value = score1;
[myScoreValue reportScoreWithCompletionHandler:^(NSError *error){
if(error != nil)
{
CCLOG(#"Score Submission Failed");
} else
{
CCLOG(#"Score Submitted");
}
}];
}
I think you need to use an int64_t for your method! I use this method and it works perfectly fine :-)
-(void)submitScore:(int64_t)score category:(NSString*)category{
GKScore *gkScore = [[[GKScore alloc]initWithCategory:category]autorelease];
gkScore.value = score;
[gkScore reportScoreWithCompletionHandler:^(NSError* error)
{
[self setLastError:error];
bool sucess = (error == nil);
[delegate onScoresSubmitted:sucess];
}];
}
Greetings
Anselm