GameKit Turn-based listener is not reliably called when matchData changes - ios

I am using Game Center turn-based matches for my card game. It feels like a good fit because people want to check their email or write a text sometimes between turns. Some users have said that they do really want to be able to have a very asynchronous playing experience.
For the people that are keeping the game open between their turns, I want to update the screen to reflect things that the other players are doing on their turns. I have set up a listener on GKLocalPlayer that responds to player:receivedTurnEventForMatch:didBecomeActive. The documentation says that this will get called when match data is saved by another player even if it doesn't become the player's turn (the player on the current device). That doesn't appear to be true 100% of the time. In fact, it appears that it only gets called about 1 in 3 times that match data is saved by other players. It seems more reliable when it becomes the player's turn, but even that isn't 100% reliable.
I am using saveCurrentTurnWithMatchData:completionHandler: on GKTurnBasedMatch to save match data that doesn't end the current player's turn and I'm calling endTurnWithNextParticipants:turnTimeout:matchData:completionHandler: on GKTurnBasedMatch to save the data when it does end the current player's turn. There are a few scenarios where I want to call saveCurrentTurn… with updated matchData. In my game, you can have computers playing in your multiplayer game as well. So, a human player may play a card and then a computer may play a card before that GKPlayer's turn ends. There are also scenarios where an individual player may play twice. (eg. A player plays the last card on a trick. That player takes the trick and gets to lead the next trick.)
I have set up a ton of logging around this and I can see clear scenarios where one device calls saveCurrentTurn… and the completionHandler is called without an error and the other device doesn't get notified with a call to player:receivedTurnEvent… I have also added logging to verify that each time I'm calling saveCurrentTurn… that I'm calling it with new matchData. I'm not making redundant calls.
If I go to the device that didn't get the updated matchData and force it to load the matchData for the match again, it gets the updated data. So, it's definitely getting saved.
I have tried throttling the calls to saveCurrentTurn… so that they don't happen in immediate succession and that didn't help.
Both devices in my testing are running iOS 8.4. There appears to have been an issue in iOS 8.3 that is fixed now (see this question). This Apple forum post also reports this issue 2 years ago and it appears that bug reports were filed and marked fixed.
Has anyone else seen this? I would love to know that I'm doing something wrong. Any ideas are very welcome.

I do something similar. In my game, each player has multiple pieces, saving the match when each piece moves so that other players--if they're in the game--can watch what's happening in real time. Like you describe, the Game Center messaging is almost completely useless.
As you referenced, in 8.3, the "end of turn" messages were completely broken. As of 8.4, they happen most, but not all, of the time. As you're seeing, the "match has been saved" notifications are also erratic. Here are some tips I've used to increase the success rate:
Slow down the saves. If you save too fast, only the last one arrives at the recipient. I set up an NSArray queue, and each time I want to save the match, I add the new matchData to that queue. I have a timer loop running that does the actual saveCurrentTurnWithMatchData, pops the item off the stack if the save was successful, and then sets up a new timer to call itself again a little later. I'm using 2 second intervals which seems to be working well.
Append each new piece of data, don't overwrite. Put a sequence number on each piece of data. So, if you save seq numbers 1, 2, 3 and 4, but the recipient only receives a notice for #4, the records for 1, 2 and 3 are there in the match object. The recipient needs to track the last record it read, and then iterate through any new records from that point when it receives an updated matchData.
I also use the queue's NSArray writeToFile: function to maintain a list of the pending saves. If the user exits the game before the queue is flushed, I reload the queue NSArray from disk at the next startup
Note that even with this mechanism, the notifications to the recipient are erratic. Generally speaking, they arrive in batches of 4+. Then nothing happens until 3 or 4 more saves happen, which again all show up together. Making 1 save and letting the game sit for 10 minutes will probably never generate a notice on the recipient's machine. But, if you save 4 or 6 times in a row, all of them tend to show up in a burst.
Sometimes, the notifications just stop for a few hours. Not sure if this a sandbox flaw or a game-center-in-general flaw. There are no failures of any kind, the messages just stop working for a while. Sometimes, the next morning, they show up in a burst. Sometimes not. In the end, I've stopped relying on the notifications. I set up another timer loop to continuously download the match. It checks if it has become my turn or not, it checks if new updates have been added to the matchData. Then calls player:receivedTurnEventForMatch:didBecomeActive. As far a receivedTurnEventForMatch: knows, it was launched because of an event and it merrily goes on about its business.
It does seem that saving the match is pretty prompt. If you don't get an error, it seems pretty certain that the updated match is immediately available for other players to consume... they just need to know to consume it. The messaging framework, though, has to be viewed as completely unreliable and non-guaranteed. Hence, the timer loop to continuously poll the match.
Edit: arguably, once I implemented #2, #1 shouldn't really matter. Any notification received by the recipient will trigger reading all new records in the data. But, this "hardening" has evolved over the past few months as I wrestle with Game Center's shortcomings. I just haven't gotten around to removing #1.

Related

In iOS11, how to keep a background task running past 10 min?

My question involves keeping an app that monitors user interactions in the background, for example time spent in one or the app. The issue arises when you can not have a background process run for more than 10 min or violate Apple's sandbox restrictions. Since I am relatively new to the Apple API, and could not find a direct answer that didn't involve location services or VOIP (which are both interesting options, but my application will not be able to use either viably), I come to ask for options in which I can understand when another app opens, when it closes, when it fetches data, and when user holds phone in certain orientation (ie when people hold their phone at certain angles to read text and etc.) for certain amount of time.
The purpose of this analyzation is to predict an attention span for the user, so I need to be able to run in the background, as the user will not be using my app while it predicts attention span.
My thoughts on this are possibly accessing the system log, and somehow parse previous statements (I don't think sandbox will allow), but inevitably the iOS system will suspend my processes after some point unless I put a timer. There is also the option of having the system wake up my app via opportunistic fetching, but that won't be useful if I don't collect the data.
Keep in mind this is within IOS 11, so it is much more restrictive than previous iterations. I understand this may seem like a complex problem, but even a direction in which to head could be useful to me.
This solution might work, (not recommended since it drains the battery quicker).
Just update your current location, every 10 mins. It will reset the background thread timer.

How to detect when Game Center turn based match has ended in iOS9?

I'm totally lost with how to implement a turn-based game. I've been trying to use the GKLocalPlayerListener methods to handle turn-based in iOS9. The only method that ever fires is receivedTurnEventForMatch, which leaves me with no method that I know of that calls an end game routine for each player. I am trying to handle turn-based matches inside of my app using the Game Center match maker view controller and delegate methods. I read that GKLocalPlayerListener methods work for matches when going through the actual Game Center app (Apple docs don't mention this). So if that's true, then GKLocalPlayerListener is not an option for my app?
What can I do to detect when a match ends? I want to keep a win-lose record for each player, so it's important that a routine is called for each player when a match ends.
The lifecycle of a turn based match looks like this:
Create a match
Invite others
Others join
Players take turns and pass the match object around (your game logic decides the order)
Players send exchanges back and forth (optional)
Players start leaving
----because they have been eliminated
----because they quit
----because they timed out
Someone Wins
If you are not the active player, you are notified when steps 3, 4, 7, 8 and 9 happen by playerReceivedTurnEventForMatch firing; however, as you can see from my answer here https://stackoverflow.com/a/34458493/1641444 playerReceivedTurnEventForMatch fires for a lot of different conditions, and it doesn't tell you which one triggered it. You have to discern that based on the status of the players, which player is the active player, and whatever other information you track in the match object.
You are notified of #5 by playerReceivedExchangeRequest firing (and Replies and Cancellation functions).
Your game logic will decide when to trigger #7. My preference is that the match object comes to the eliminated player, they are recognized as defeated, and calls participantQuitInTurnWithOutcome
Players decide when to trigger #8 by quitting, and the code calls either participantQuitInTurnWithOutcome or participantQuitOutOfTurnWithOutcome depending on their state.
Condition #9 is a real pain in the ass. Between limitations in the game design and outright bugs, timeouts can create several unrecoverable edge cases. Handling timeouts warrants its own full answer.
Finally, #10 is triggered by calling endMatchInTurnWithMatchData. GKTurnBasedEventHandlerDelegate and handleMatchEnded were deprecated in IOS7. When using GKLocalPlayerListner, you'll be notified of #10 by yet another occurrence of playerReceivedTurnEventForMatch
Edit--Clarifications based on followup questions:
Yeah, exactly. In your sendTurn function, when the game is over, don't call endTurnWithNextParticipant. Instead, set the each participant's status to indicate who won and who lost, and then call endMatchInTurnWithMatchData.
The other players will see playerReceivedTurnEventForMatch fire. You can discern from the match status and the player status that the game is over and decide what actions to take for that recipient.
To end a match, you should have your game call endMatchInTurnWithMatchData:completionHandler:
If you've implemented GKTurnBasedEventHandlerDelegate protocol on an object, your handleMatchEnded will get a push event.
More details in Apple's programming guide

removeWithCompletionHandler causes logging out of Game Center

I am using GKTurnBasedMatch's removeWithCompletionHandler to programmatically remove old (finished, i.e., with status = GKTurnBasedMatchStatusEnded) turn-based matches from Game Center when needed (to avoid extra loading burden, I want to keep the amount of finished matches to a minimum).
I am getting no error and the match is correctly removed.
However, most times I do this, my user gets kicked out of Game Center, so the local player is no longer authenticated. This happens on iOS7, with both iPhone and iPad.
Did anyone experience this? Is there any way around it?
NB: I might as well not remove matches from GC, as I use internal business logic to determine which matches should be listed to the user and show, say, only the 10 (unfinished) most recent ones. However, I am afraid that hundreds of games might be kept in GC and that this might slow down interactions with GC when I have to list ongoing matches.
If you're calling removeWithCompletionHandler inside of a completion handler for another Game Center API call then you might be experiencing the very same issue that's been plaguing me for a while. The solution I tried today involves simply delaying the call to removeWithCompletionHandler for a few seconds, like so:
[match performSelector:#selector(removeWithCompletionHandler:) withObject:^(NSError *error) { /* callback code here */ } afterDelay:3.0];
For me, this worked great but meant I had to rework a few things in relation to displaying active games... so be wary of that if this solution works for you. Also, it's worth noting that the 3 second delay is an arbitrary value I picked and it worked for me.
In my case, I was calling removeWithCompletionHandler inside the completion handler block for the various quit methods on GKTurnBasedMatch.
I imagine there is some issue on Game Center's end where the two requests cannot be made so close together without resulting in such woe. This is evidenced by the fact that the issue did not ever occur for me when I slowly stepped through the calls to Game Center.
EDIT
Here's some cool news; seems like they've fixed the bug in iOS 8. I built an app which demonstrates the bug for bug-reporting purposes (originally for iOS 7). I've tested that same app running in iOS 8 and it seems that the bug has been squashed.
Finally.
IMO, you should consider adding the removeWithCompletionHandler call in now.

iOS touchesMoved delay between first and second calls

I have some problems in my iOS app.
I have checked timestamps in the touchesMoved with clock() function and use difference between current and previous values. Difference between 1st and 2nd events is greater than others.
Have you any ideas?
iOS is not a hard-realtime OS. There is generally no expectation that touch events will be delivered with regular frequency/uniform periods. You should not structure your app to depend on that. Events are delivered via the main thread, which could be delayed by other processing (drawing, etc.)
EDIT: If you're seeing huge differences in periods between the 1st and 2nd touches (relative to the periods between subsequent touch events), the first step would be to run the app in Instruments' Profiler template to see if some work that you're doing on the main thread in response to the first touch is causing the delay. If the delay is of your own doing, then fixing that is the first order of business.
Beyond that, you could try using various signal processing approaches to re-sample your data into uniform periodic data, but the problem is that any of those algorithms won't be able to deliver the first re-quantized point until at LEAST the 2nd (and more likely the 3rd or even later) event comes in, so if the principal problem is "The 2nd event doesn't come as soon as you'd like", then this won't help you.
Another good test: Try making an empty single-view sample app that does nothing but log the events as they come in -- if you see the same "odd" delay between 1st and 2nd touches in that trivial sample app, then that's just "how it is," and you can stop wasting time trying to change the OS behavior and move on to some different approach/solution.

how to use GANTracker for long running iOS apps, including background audio

My App is typically run overnight as a baby monitor, either as foreground app, or with background audio running.
Goals:
Track total app startups ie. active user count.
Track total usage time in foreground vs background and total session time.
Track various page-views if they navigate the settings screens.
As recommended, I start the tracker in didFinishLaunchingWithOptions, and track my first ViewController as my first 'page-view'. My App might stay on this page then for the next 8 hours...
A couple of issues then appear:
When do I call stopTracker and what does it do? I'm hoping that it terminates the tracking session. But since google kindly hid their code in a static lib, I have no idea what's going on under the covers, and the .h doesn't say much. First instinct is to put stopTracker in applicationWillResignActive however, if the user decides to enable background audio my app is still running...
Next I read that a session can timeout after 30mins with no new pageviews, or at midnight. I could set a repeating timer to send the same page-view every 20mins, that should keep my session alive, at least until midnight, but then my page views are going to be much larger? unless it's smart enough to know I'm on the same page with every call. google analytics blog
[Update: each call seems to be counted as a new pageview, and numbers are thus skewed, so still an issue how to handle this]
If my timer above runs past midnight and the session has expired, I'm going to end up with a new session and double the actual active user count?
If I do call stopTracker in applicationWillResignActive, will the next call to track a page-view restart the tracker? or do I need to call startTrackerWithAccountID again?
If instead I start the tracker in applicationDidBecomeActive, I lose the session that might have been running in the background.
[update: this seems to be the best approach so far, but testing is very slow due to time lag on analytics reports, I will report back soon]
PS EasyTracker doesn't seem to handle this any better.
I got this working by using a pageview called 'Backgrounded', and when the user has selected no background functionality, then instead the app is calling stopTracker. I see multiple hits, with an average session of 20mins, but i can multiple pageview by time to see total time for goal 2. I found two solutions for goal one, events (which were not exposed in easy tracker), and also in my applicationDidBecomeActive (if it's not a restore of backgrounded app) then i track a pageview for AppStarted. I ended up wrapping the whole thing in a utility class and rolled it into a couple of my apps, so will be interesting too see the results. If anyone else tries this, you might want to think about using the custom variables too. I added my app version to this, so I can also monitor how many users are migrating to the latest app releases.

Resources