As the apple guidelines said; I've implemented the GKLocalPlayerListener protocol in my game center class and add the local player as listener as soon as he's authenticated:
func authenticationChanged() {
if (GKLocalPlayer.localPlayer().authenticated && !self.userAutenticated) {
println("Authentication changed: player authenticated.")
userAutenticated = true
GKLocalPlayer.localPlayer().unregisterAllListeners()
GKLocalPlayer.localPlayer().registerListener(self)
} else if (GKLocalPlayer.localPlayer().authenticated && self.userAutenticated) {
println("Authentication changed: player not authenticated.")
userAutenticated = false
}
}
Protocol implementation:
// MARK: - GKLocalPlayerListener
func player(player: GKPlayer!, didAcceptInvite invite: GKInvite!) {
println("Did accept invite")
}
func player(player: GKPlayer!, didRequestMatchWithRecipients recipientPlayers: [AnyObject]!) {
println("Did request matchmaking")
}
None of this 2 methods it's called when I try to invite a friend and I also didn't receive any kind of notifications.
I've tried to test the game in release mode but I've got the same result.
I must say the the normal matchmaking works correctly, I'm able to find player to play with without any problems.
EDIT:
If i test test it from 2 devices, the notification will be received but if i tap on the notification, the app will be open and no delegate will be called. If i close the app and restart it again, then the GKLocalPlayerListener it's called.
What's wrong??
I assume that, when you say "the normal matchmaking works" that you have presented a matchmakerviewcontroller:
-(IBAction)setupMatch:(id)sender{
GKMatchmakerViewController *matchViewController = [[GKMatchmakerViewController alloc] initWithMatchRequest:matchRequest];
matchViewController.matchmakerDelegate = self;
[self presentViewController:matchViewController animated:YES completion:nil];}
Then when the players are found in the matchmakerviewcontroller didFindMatch will be called:
-(void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)match{
//Called when GameCenter completes the matchmaking process
match.delegate = (id)self; //etc. lots of your own code here.
didAcceptinvite is only called on the device of the recipient of an invite after they accept the invite:
-(void)player:(GKPlayer *)player didAcceptInvite:(GKInvite *)invite{
//Called on the accepting device when the invitation is accepted
GKMatchmakerViewController *mmvc = [[GKMatchmakerViewController alloc] initWithInvite:invite];
mmvc.matchmakerDelegate = self;
[self presentViewController:mmvc animated:YES completion:nil];
}
That presents a matchmakerviewcontroller to your friend. There is nothing for them to do, the vc makes the connection and then dismisses itself. The vc on the senders device dismisses at the same time.
Then didFindMatch is called on both devices and away you go.
I am not convinced didrequestMatchWithRecipients is ever called and it seems redundant when didFindMatch and didAcceptInvite deal with the game start at both ends.
I found this video from WWDC 2012 really helpful: WWDC 2012 Christy Warren
Related
I have a little game with a timer.
I'm implementing adMob to monetize and I am not able to restart timer/ads after user clicks on the banner and come back to the app.
The flow is:
1 - game start
2 - show ads
3 - click on banner and pause timer
4 - oper safari
5 - click "back to my app" link/button (iOS feature)
6 - back to the app and restar timer (problem here)
I had implemented all adMob events method (and insert restar timer code) but I can't get out of this issue.
The code work because it worked with iAds (I'm migrating to adMob).
Any help is appreciated.
Thank you
EDIT:
here is the code:
/// Tells the delegate an ad request loaded an ad.
- (void)adViewDidReceiveAd:(GADBannerView *)adView {
NSLog(#"adViewDidReceiveAd");
self.pauseTimer = NO;
}
/// Tells the delegate an ad request failed.
- (void)adView:(GADBannerView *)adView
didFailToReceiveAdWithError:(GADRequestError *)error {
NSLog(#"adView:didFailToReceiveAdWithError: %#", [error localizedDescription]);
self.pauseTimer = NO;
}
/// Tells the delegate that a full screen view will be presented in response
/// to the user clicking on an ad.
- (void)adViewWillPresentScreen:(GADBannerView *)adView {
NSLog(#"adViewWillPresentScreen");
self.pauseTimer = NO;
}
/// Tells the delegate that the full screen view will be dismissed.
- (void)adViewWillDismissScreen:(GADBannerView *)adView {
NSLog(#"adViewWillDismissScreen");
self.pauseTimer = NO;
}
/// Tells the delegate that the full screen view has been dismissed.
- (void)adViewDidDismissScreen:(GADBannerView *)adView {
NSLog(#"adViewDidDismissScreen");
self.pauseTimer = NO;
}
/// Tells the delegate that a user click will open another app (such as
/// the App Store), backgrounding the current app.
- (void)adViewWillLeaveApplication:(GADBannerView *)adView {
NSLog(#"adViewWillLeaveApplication");
self.pauseTimer = YES;
}
In this VC create a property to store this
#property (nonatomic) BOOL didGoToSafari;
- (void)adViewWillLeaveApplication:(GADBannerView *)adView {
NSLog(#"adViewWillLeaveApplication");
self.pauseTimer = YES;
self.didGoToSafari = YES;
}
In the VC that you show right before the ad would show in viewWillAppear or viewDidAppear you should put this code
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(applicationDidBecomeActiveNotification:)
name:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];
And then after viewDidAppear or viewWillAppear, write this function
- (void)applicationDidBecomeActiveNotification:(NSNotification *)notification {
if (self.didGoToSafari = YES){
self.pauseTimer = NO;
self.didGoToSafari = NO;
}
}
In viewWillDisappear
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];
Basically what you're doing is listening to see if the app became active again. If it did, check to see if it's coming back from Safari. It's not perfect because you could feasibly be using the app, user goes to Safari and then doesn't go back to or close the game. They could then use Safari later and then go back to the game and it would start running again. There probably some control flow in the AppDelegate you could use to code around this, but in general this code should do it.
EDIT: As per your comment about understanding it, here's the full explanation.
You are using NSNotification to listen for when the app returns to an active state. UIApplicationDidBecomeActiveNotification is automatically called when your app becomes active (it's an app delegate method). When it does, the method (void)applicationDidBecomeActiveNotification gets called automatically and the methods in that method get called. You have a boolean flag to see if the app is returning from Safari because your app could return from any other app if user switched to another app when the ad got pushed. In the end, you remove your VC as an observer to avoid memory leaks.
In a nut shell, I have three views: Main View, List View (to display the buddies list) and Chat View(where you chat).
The push notification is correctly setup in this app, so I have no problems in receiving the push notification payload, my question lies on why in a specific scenario the received message seemed lost and doesn't appear in Chat View
I have a strange problem, so please carry on read the whole description:
So here I put some code to show how I handle the push notifications:
In AppDelegate.m
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
// Extract the Sinch-specific payload from the Apple Remote Push Notification
NSString* payload = [userInfo objectForKey:#"SIN"];
[self initSinchClientWithUserId:userid];
// Get previously initiated Sinch client
id<SINNotificationResult> result = [_client relayRemotePushNotificationPayload:payload];
if ([result isMessage]) {
// Present alert notifying
NSString *messageId = [[result messageResult] messageId];
NSLog(#"Received messageid: %#", messageId);
if([UIApplication sharedApplication].applicationState == UIApplicationStateBackground ||
[UIApplication sharedApplication].applicationState == UIApplicationStateInactive) {
self.messageSenderId = [[result messageResult] senderId];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ReceivedRemoteNotification" object:self userInfo:nil];
NSLog(#"handle remote notification in modal");
} else {
self.messageSenderId = [[result messageResult] senderId];
// this will launch a modal of private chat, so better to invoke this when running in background
[[NSNotificationCenter defaultCenter] postNotificationName:#"SetUnreadIcon" object:self userInfo:nil];
NSLog(#"handle remote notification by marking icons");
}
} else if (![result isValid]) {
// Handle error
}
NSLog(#"Received Payload: %#", payload);
}
In Main View, handles ReceivedRemoteNotification and SetUnreadIcon
-(void) viewDidLoad {
[super viewDidLoad];
....
// setup a local notification handler
NSNotificationCenter *notifCentre = [NSNotificationCenter defaultCenter];
[notifCentre addObserver:self selector:#selector(invokeRemoteNotifications:) name:#"ReceivedRemoteNotification" object:nil];
[notifCentre addObserver:self selector:#selector(setUnreadIcon:) name:#"SetUnreadIcon" object:nil];
....
}
- (void)invokeRemoteNotifications: (NSNotification *) notification {
shouldDismiss = NO;
[self invokeNotifications];
}
- (void)invokeNotifications {
[self performSegueWithIdentifier:#"modalToChat" sender:self];
}
- (void)setUnreadIcon: (NSNotification *) notification {
AudioServicesPlaySystemSound (1300);
shouldDismiss = YES;
[self.rightbutton setCustomView:notifyButton];
}
When user tap rightbutton in MainView, will call up the ListView.
In ChatView, it implements SINMessageClientDelegate delegate methods (See Documentation), namely
– messageClient:didReceiveIncomingMessage:
– messageSent:recipientId:
– messageDelivered:
– messageFailed:info:
– message:shouldSendPushNotifications:
The Working Scenario
Assume user Alice send message to Bob. Bob received this message via push notification. So Bob open and tap the notifications, the app will show up and automatically showing a modal view of ChatView.
In this case, the message from the push notifications can be displayed in Chat View
The Not Working Scenario
Assume user Alice send msg to Bob again, this time Bob received the notification while the app is in foreground. So this method will be executed:
- (void)setUnreadIcon: (NSNotification *) notification {
AudioServicesPlaySystemSound (1300);
shouldDismiss = YES;
[self.rightbutton setCustomView:notifyButton];
}
Then Bob will tap rightbutton to call up ListView, find Alice and open the conversation. Bob will find that the message just received from Alice is not displayed in ChatView
Questions
In Sinch SDK, there seemed no approach to manually retrieve the messages even the messageId is retrieved while receiving the push notification. Am I correct?
In the "Not Working" case, why the message is lost? If the sinch client is responsible for relaying the messages, what reason to make it discard the messages?
Still in the "Not Working" case, how I can persist the messages, and then later display it? Must I implement the SINMessageClientDelegate elsewhere?
Thanks,
Regarding your first question, there is no way to query for a particular message in that way you are describing.
The problem for your "Not Working"-case is likely that by the time the Sinch SDK fetches the instant message and is about to notify the message client delegate, your Chat View has not yet been instantiated / assigned to be the delegate, and because of that the message is "missed" by the delegate.
I would recommend you to have a non-ViewController component implement the SINMessageClientDelegate, and make that component have a life-cycle that is independent of your view controllers. I.e. have the component acting as the delegate be created at application launch, and keep it alive. Then you can be sure to always receive all onIncomingMessage:, and you can also place logic for persisting messages there.
I am trying to integrate Game Center for matching players. I am using that very simple function:
- (void)findOpponent {
GKMatchRequest* request = [[GKMatchRequest alloc] init];
request.minPlayers = 2;
request.maxPlayers = 2;
GKMatchmakerViewController *mmvc = [[GKMatchmakerViewController alloc]
initWithMatchRequest:request];
mmvc.matchmakerDelegate = self;
[[self viewController] presentViewController:mmvc animated:YES completion:nil];
}
I do have some callback, never called tho':
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)theMatch {
NSLog(#"he");
[[self viewController] dismissViewControllerAnimated:NO completion:nil];
GKMatch* match = theMatch;
[match setDelegate:self];
NSLog(#"Ready to start match!");
}
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindPlayers:(NSArray *)playerIDs {
NSLog(#"Super he");
}
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didReceiveAcceptFromHostedPlayer:(NSString *)playerID {
NSLog(#"Wow");
}
When I test onmy device (wether iPhone or iPad), both using iOS 7.1, I can open Game Center from my Application, but as soon as I click on "Play Now" I instantaneously get the error: "Failed to find players". Yet none of my callback seems to get triggered in my code. Any idea of what I am doing wrong? I did try to reset my iPad settings, logging off from iCloud, rebooting my device, etc.
Fixed.
More than creating a Provisioning Profile and enabling Game Center inside, you also need to Add your app on iTunes Connect.
You need to go to an extremely strange process where it ask for the release date of your app, the price, some screen shots and icons, but once done you will be able to enable Game Center for real.
The documentation for CTCallCenter:setCallEventHandler: states that:
However, call events can also take place while your application is
suspended. While it is suspended, your application does not receive
call events. When your application resumes the active state, it
receives a single call event for each call that changed state
The part relevant to this question is
When your application resumes the active state, it receives a single
call event for each call that changed state
Implying the app will receive a call event for a call that took place in the past while the app was suspended. And this is possible according to the answer to this question: How does the Navita TEM app get call log information?
My question is: if my app is suspended and a call takes place, then when my app resumes the active state how can it retrieve the call event for the call that took place?
I have tried many, many code experiments but have been unable to retrieve any call information when my app resumes the active state.
This is the most simplest thing I have tried:
1) Create a new project using the Xcode single view application template.
2) Add the code shown below to didFinishLaunchingWithOptions
3) Launch the app
4) Task away from the app
5) Make a call from another device, answer the call, hang up the call from either device
6) Bring the app back to the foreground thus resuming the active state.
The code to register for call events is:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.callCenter = [[CTCallCenter alloc] init];
[self.callCenter setCallEventHandler:^(CTCall *call)
{
NSLog(#"Event handler called");
if ([call.callState isEqualToString: CTCallStateConnected])
{
NSLog(#"Connected");
}
else if ([call.callState isEqualToString: CTCallStateDialing])
{
NSLog(#"Dialing");
}
else if ([call.callState isEqualToString: CTCallStateDisconnected])
{
NSLog(#"Disconnected");
} else if ([call.callState isEqualToString: CTCallStateIncoming])
{
NSLog(#"Incomming");
}
}];
return YES;
}
With this code I am able to get call events if the app is in the foreground when the call occurs. But if I task away from the app before making the call then I am unable to get a call event when my app next resumes the active state - as it states it should in the Apple documentation.
Other things I have tried:
1) The documentation states that the block object is dispatched on the default priority global dispatch queue, so I have tried placing the registration of setCallEventHandler within dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{})
2) Calling setCallEventHandler: in appBecameActive instead of didFinishLaunchingWithOptions
3) Adding background abilities to the app - via beginBackgroundTaskWithExpirationHandler and/or location updates using startUpdatingLocation or startMonitoringForSignificantLocationChanges.
4) Various combinations of the above.
The bounty will be awarded once I get code running on my device which is able to get call events that took place while the app was suspended.
This is on iOS 7.
I've found a solution but I have no idea why it's working. Only thing I can think of is a bug in GCD and/or CoreTelephony.
Basically, I allocate two instances of CTCallCenter like this
void (^block)(CTCall*) = ^(CTCall* call) { NSLog(#"%#", call.callState); };
-(BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
callCenter1 = [[CTCallCenter alloc] init];
callCenter1.callEventHandler = block;
callCenter2 = [[CTCallCenter alloc] init];
callCenter2.callEventHandler = block;
return YES;
}
Similar Code in Swift:
func block (call:CTCall!) {
println(call.callState)
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//Declare callcenter in the class like 'var callcenter = CTCallCenter()'
callcenter.callEventHandler = block
return true
}
To test this I made a call, answered it and then hanged up it while app was in background. When I launched it I received 3 call events: incoming, connected, disconnected.
In my case, I was working on an enterprise app which doesn't need to be approved by Apple's app market - so if you develop an enterprise app this solution is for you.
Also, the chosen answer didn't work while the app is the background.
The solution is simple, basically you just need to add 2 capabilities (VOIP & Background fetch) in the Capabilities tab:
Your project target -> Capabilities -> Background Modes -> mark Voice over IP & Background fetch
Now, your app is registered to the iOS framework calls "delegate" so the OP code snip solution:
[self.callCenter setCallEventHandler:^(CTCall *call)
{
NSLog(#"Event handler called");
if ([call.callState isEqualToString: CTCallStateConnected])
{
NSLog(#"Connected");
}
else if ([call.callState isEqualToString: CTCallStateDialing])
{
NSLog(#"Dialing");
}
else if ([call.callState isEqualToString: CTCallStateDisconnected])
{
NSLog(#"Disconnected");
} else if ([call.callState isEqualToString: CTCallStateIncoming])
{
NSLog(#"Incomming");
}
}];
Would defiantly work and you will get notifications even if your app is in the background.
I have the following code that I have been using before to handle invitations:
[GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) {
// Insert game-specific code here to clean up any game in progress.
if (acceptedInvite) {
GKMatchmakerViewController *mmvc = [[GKMatchmakerViewController alloc] initWithInvite:acceptedInvite];
mmvc.matchmakerDelegate = self;
[self presentViewController:mmvc animated:YES completion:nil];
}
};
However, now it has become deprecated in iOS 7. Where and how do I register a GameKit invite handler in my project?
GKInviteEventHandler to the rescue, and in particular take a look at GKLocalPlayerListener.
Conform to the GKLocalPlayerListener protocol and you should be OK. Below are the protocol methods, which look to be the intended replacement for invitationHandler, but split up in two parts.
- (void)player:(GKPlayer *)player didAcceptInvite:(GKInvite *)invite
- (void)player:(GKPlayer *)player didRequestMatchWithPlayers:(NSArray *)playerIDsToInvite
After you set up some object to conform to that, you just make a call to registerListener:.
[[GKLocalPlayer localPlayer] registerListener:yourObjectHere]
Don't worry about registering it as soon as possible, as the system caches the invites/challenges/turn based stuff, if there's no one to handle those and lets your listener know as soon as you set it up.
As Sam says the new way to do this is with the GKLocalPlayerListener protocol. The approach is reversed now. In the past you issued invitations to other players from part of your app. The other part listened for an invitation from another player and responded to that. Now, you use a matchMakerViewController or Game Center to issue invitations (as before) but now you listen for the acceptance of those invitations. After that Game Center calls didFindMatch to get everything started. If you are in receipt of an invitation Game Center starts your game and then calls didFindMatch to start it up.
This is my code:
In my .h file the GKLocalPlayerListener protocol:
#interface MNFStartupViewController : UIViewController<ADBannerViewDelegate, GKMatchmakerViewControllerDelegate, GKMatchDelegate, GKLocalPlayerListener, UIAlertViewDelegate>
in the .m file in my authenticateHandler block after the local player is authenticated:
[[GKLocalPlayer localPlayer] registerListener:self];
Then the method to listen for the acceptance of an invite:
-(void)player:(GKPlayer *)player didAcceptInvite:(GKInvite *)invite{
//Called when another player accepts a match invite from the local player.
NSLog(#"didAcceptInvite was called: Player: %# accepted our invitation", player);
GKMatchmakerViewController *mmvc = [[GKMatchmakerViewController alloc] initWithInvite:invite];
mmvc.matchmakerDelegate = self;
[self presentViewController:mmvc animated:YES completion:nil];}
Now the method to start the game from Game Center with a set of players chosen in Game Center. This is hard to debug because you cannot start the game in Game Center whilst running it from Xcode at the same time (I don't think so anyway!) so there is a debugging AlertView that can be dropped.
-(void)player:(GKPlayer *)player didRequestMatchWithPlayers:(NSArray *)playerIDsToInvite{
//Called when the local player starts a match with another player from Game Center
//Start of debugging logging and alerting
NSLog(#"In didRequestMatchWithPlayers for players: %#", playerIDsToInvite);
NSString *logString = [[NSString alloc] initWithFormat:#"didrequestMatchWithPlayers was called with player IDs: %#", playerIDsToInvite];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Logging Alert" message:logString delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
//End of debugging logging and alerting
//Create a match for the chosen players
GKMatchRequest *match = [[GKMatchRequest alloc]init];
match.playersToInvite = playerIDsToInvite;
//Create a matchmaking viewcontroller for that match
GKMatchmakerViewController *mmvc = [[GKMatchmakerViewController alloc]initWithMatchRequest:match];
mmvc.matchmakerDelegate = self;
[self presentViewController:mmvc animated:YES completion:nil];}
This is a method to kick off the whole matchmaking process:
-(IBAction)setupMatch:(id)sender{
GKMatchmakerViewController *matchViewController = [[GKMatchmakerViewController alloc] initWithMatchRequest:matchRequest];
matchViewController.matchmakerDelegate = self;
[self presentViewController:matchViewController animated:YES completion:nil];}
Finally this is the method called by Game Center to set the match going when all the players are connected and ready to go. currentPlayer, currentMatch and hostingPlayer are my own properties with obvious uses.
-(void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)match{
//Called when GameCenter completes the auto matchmaking process
match.delegate = (id)self;
[self setCurrentMatch:match];
[self setCurrentPlayers:match.playerIDs];
NSLog(#"Match was found with players: %#, time to get on with the game.", self.currentPlayers);
//Use the built in features to decide which device should be the server.
self.hostingPlayer = [self chooseHostingPlayerIDfromPlayerIDs:self.currentPlayers];
[self dismissViewControllerAnimated:YES completion:nil];}
Hope it helps.
Also, invites only work on the devices. I could not get invites to work in the simulator in iOS 8.1.
I believe the answer is, annoyingly, different for a GKMatch and a GKTurnBasedMatch.
For a GKTurnBasedMatch the invitation counts as a turn event, and is handled in this function:
player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool)
That's inside the GKLocalPlayerListener protocol. You have to have officially registered GKLocalPlayerListener instance with your local player for this to work, so you can only do it after authentication.
...and it doesn't always work. Game Center is unreliable. But it does work sometimes, and that's... something?