Why does Social.localUser.Authenticate lead to crash when there is no internet connection in Unity app? - ios

With an internet connection
Everything works flawlessly. There is no memory problem leading to crash.
With no internet connection
The app proceeds to the menu screen, where it eventually crashes because it is out of memory.
I have concluded that the problem lies in the following line of code
Social.localUser.Authenticate
When I comment out the above line, the memory problem goes away when there is no internet connection.
Here is my relevant code
void Start ()
{
Social.localUser.Authenticate(ProcessAuthentication);
}
public void ProcessAuthentication(bool success)
{
if(success)
Debug.Log ("Authenticated");
else
Debug.Log ("Failed to authenticate");
}
Leading up to the crash
2016-02-27 15:46:37.131 BrickBall[449:60670] Received memory warning.
WARNING -> applicationDidReceiveMemoryWarning()
2016-02-27 15:46:37.302 BrickBall[449:60670] Received memory warning.
WARNING -> applicationDidReceiveMemoryWarning()
2016-02-27 15:46:37.349 BrickBall[449:60670] Received memory warning.
WARNING -> applicationDidReceiveMemoryWarning()
2016-02-27 15:46:37.437 BrickBall[449:60670] Received memory warning.
WARNING -> applicationDidReceiveMemoryWarning()
Message from debugger: Terminated due to memory issue
Why would that line of code be causing the out of memory crash when there is no internet connect?

My guess is that you'll eventually need to talk to Unity. Game center will use cached credentials when there's no network connectivity to report that it successfully connected to the server and authenticated, even though it didn't. I have a bug open--and an ongoing discussion--with Apple on this. This behavior allows some game types to continue even when there's no network, then sync up later when connection is restored. However, I ran into problems where I assumed I could do things because GC said it was authenticated, but I really couldn't because it really wasn't. :/
This means apps have to handle three cases:
successful authentication with GC
failed authentication with GC
failed authentication, but reported as successful based on cached data
It's possible that Unity doesn't handle the third situation. To confirm or refute this, try the following:
Confirm that Unity does cleanly handle authentication failures
establish connectivity
log out of game center
Break connectivity (airplane mode, etc)
Retry your app
I would expect that success would be false at this point and run cleanly.
If that works as expected, I'd talk to Unity about how they handle Game Center reporting a (cached) success in a disconnected situation.
Edit2:
I had to go back and look at my code to see exactly how I hardened against it. The scenario was: while completely disconnected and/or in airplane mode, Game Center was presenting the "welcome back" message and localPlayer.authenticated was set to YES... BUT, the error code was set and complaining that it couldn't connect.
I opened bug 22232706, "[GKLocalPlayer localPlayer].authenticated always returns true after any authentication handler is set," and which still has an ongoing discussion. Apple confirmed the behavior, but says its intended.
Below is how I hardened my authentication handler to deal with this situation. It won't help you since Unity is handling this for you, but I thought other readers may find this helpful. (The TL;DR version is: always always always check the error code first, before you check .authenticated or before you check if viewController is set)
[localPlayer setAuthenticateHandler:^(UIViewController *loginViewController, NSError *error)
{
//Note: this handler fires once when you call setAuthenticated, and again when the user completes the login screen (if necessary)
//did we get an error? Could be the result of either the initial call, or the result of the login attempt
//Very important: ALWAYS check `error` before checking for a viewController or .authenticated.
if (error)
{
//Here's a fun fact... even if you're in airplane mode and can't communicate to the server,
//when this call back fires with an error code, localPlayer.authenticated is sometimes set to YES despite the total failure. ><
//combos seen so far:
//error.code == -1009 -> authenticated = YES
//error.code == 2 -> authenticated = NO
//error.code ==3 -> authenticated = YES
if ([GKLocalPlayer localPlayer].authenticated == YES)
{
NSLog(#"error.code = %ld but localPlayer.authenticated = %d", (long)error.code, [GKLocalPlayer localPlayer].authenticated);
}
//Do stuff here to disable network play, disable buttons, warn users, etc.
return;
}
//if we received a loginViewContoller, then the user needs to log in.
if (loginViewController)
{
//the user isn't logged in, so show the login screen.
[rootVC2 presentViewController:loginViewController animated:NO completion:^
{
//was the login successful?
if ([GKLocalPlayer localPlayer].authenticated)
{
//enable network play, or refresh matches or whatever you need to do...
}
}];
}
//if there was not loginViewController and no error, then the user is alreay logged in
else
{
//the user is already logged in
//refresh matches, leaderboards, whatever you need to do...
}
}];

Related

WatchKit extension crash: "Program ended with exit code: 0"

For people wanting to reply quickly without reading the post: I am not hitting any memory limits. Read the whole post for details.
My WatchKit extension cannot properly function without the user first being "onboarded" through the phone app. Onboarding is where the user must accept the permissions that we require, so it's very crucial.
On my WatchKit extension, I wanted to display a simple warning for users who had not finished onboarding within our phone app yet.
As such, I thought I'd get the status of onboarding from the phone in two ways:
When the user opens the app/the app is activated (I use the willActivate method to detect this)
When the app finishes onboarding it sends a message to the watch of its completion (if the extension is reachable, of course)
Both of these combined would ensure that the status of onboarding is always kept in sync with the watch.
I wrote the first possibility in, utilizing reply handlers to exchange the information. It worked just fine, without any troubles. The warning telling the user to complete disappears, the extension does not crash, and all is well.
I then wrote in the second possibility, of the extension being reachable when the user finishes onboarding (with the phone then directly sending the companion the new status of onboarding). My extension crashes when it receives this message, and I am stuck with this odd error.
Program ended with exit code: 0
My extension does not even get a chance to handle the new onboarding status, the extension just quits and the above error is given to me.
I am not hitting any sort of memory limit. I have read the technical Q&A which describes what a memory usage limit error looks like, and I don't receive any sort of output like that whatsoever. As well, before the extension should receive the message, this is what my memory consumption looks like.
I have monitored the memory consumption of the extension right after finishing onboarding, and I see not a single spike indicating that I've gone over any kind of threshold.
I have tried going line by line over the code which manages the onboarding error, and I cannot find a single reason that it would crash with this error. Especially since the reply handler method of fetching the onboarding status works so reliably.
Here is the code of how I'm sending the message to the watch.
- (void)sendOnboardingStatusToWatch {
if(self.connected){
[self.session sendMessage:#{
LMAppleWatchCommunicationKey: LMAppleWatchCommunicationKeyOnboardingComplete,
LMAppleWatchCommunicationKeyOnboardingComplete: #(LMMusicPlayer.onboardingComplete)
}
replyHandler:nil
errorHandler:^(NSError * _Nonnull error) {
NSLog(#"Error sending onboarding status: %#", error);
}];
}
}
(All LMAppleWatchCommunicationKeys are simply #define'd keys with exactly their key as the string value. ie. #define LMAppleWatchCommunicationKey #"LMAppleWatchCommunicationKey")
Even though it's never called by the extension, here is the exact receiving code of the extension which handles the incoming data, if it helps.
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message {
NSString *key = [message objectForKey:LMAppleWatchCommunicationKey];
if([key isEqualToString:LMAppleWatchCommunicationKeyOnboardingComplete]){
BOOL newOnboardingStatus = [message objectForKey:LMAppleWatchCommunicationKeyOnboardingComplete];
[[NSUserDefaults standardUserDefaults] setBool:newOnboardingStatus
forKey:LMAppleWatchCommunicationKeyOnboardingComplete];
dispatch_async(dispatch_get_main_queue(), ^{
for(id<LMWCompanionBridgeDelegate> delegate in self.delegates){
if([delegate respondsToSelector:#selector(onboardingCompleteStatusChanged:)]){
[delegate onboardingCompleteStatusChanged:newOnboardingStatus];
}
}
});
}
}
Before including this onboarding-related code, my WatchKit extension was tested by over 100 people, without any troubles. I am using the exact same custom error dialogue that I was using before, just with a different string. I cannot for the life of me figure out what is causing this crash, and the ambiguity of it has given me very little to work with.
Any help would be greatly appreciated. Thank you very much for taking your time to read my post.
Edit: I just tried creating a symbolic breakpoint for exit(), which is never hit. If I call exit() myself, it calls the breakpoint, so I know the breakpoint itself is working.

CloudKit App to handle different iCloud accounts

I have an app that keeps user data in a private database using CloudKit and iCloud. I have written code to handle when the user logs out of iCloud and back in as a different user via Settings -> iCloud on their device. I am currently in Beta Testing mode and when I try this as an internal tester, the app crashes when I change accounts. When I test this in development mode using either the Xcode simulator or a real device, my code works great. My question is: should CloudKit apps be able to handle when the iCloud account changes on the device? If so, is this case able to be tested with internal testing via TestFlight. My code to handle account changes is below...
func isCloudAccountAvailableASync() {
container.accountStatusWithCompletionHandler { (accountStatus, error) in
switch accountStatus {
case .Available:
print("INFO: iCloud Available!")
// begin sync process by finding the current iCloud user
self.fetchUserID()
return
case .NoAccount:
print("INFO: No iCloud account")
case .Restricted:
print("WARNING: iCloud restricted")
case .CouldNotDetermine:
print("WARNING: Unable to determine iCloud status")
}
// if you get here, no sync happened so make sure to exec the callback
self.syncEnded(false)
}
}
func fetchUserID(numTries: Int = 0) {
container.fetchUserRecordIDWithCompletionHandler( { recordID, error in
if let error = error {
// error handling code
// reach here then fetchUser was a failure
self.syncEnded(false)
}
else {
// fetchUserID success
if self.lastiCloudUser == nil {
print("INFO: our first iCloud user! \(recordID)")
self.saveUserID()
}
else if !recordID!.isEqual(self.lastiCloudUser) {
// User has changed!!!!!!!!!!!!!
self.saveUserID()
// delete all local data
// reset the saved server token
self.saveServerToken(nil)
// try again with reset fields
}
// else user never changed, then just create a zone
// continue the sync process
self.initZone()
}
})
}
---EDIT/UPDATE:---
Here is a screenshot of my crash log
---EDIT/UPDATE 2:---
I was able to generate another crash with a different crash log. This crash log still doesn't point to my code, but at least describes a function...
I kept making it crash and somehow got a crash log that included a line of code I could link to a line in my Xcode project. My issue was with NSOrderedSet and iOS 9. That issue can be found here. I do not know why I only got hex in my crash log before, if anyone knows how to deal with hex crash logs I would love to hear it.
Here are the answers to my original question for anyone out there:
Should CloudKit apps be able to handle when the iCloud account changes on the device?
Answer: Yes
If so, is this case able to be tested with internal testing via TestFlight?
Answer: Yes
It's hard to say were your app crashes exactly.
You have to be aware that after an account change you should reset the state of your app (clearing local user data and navigating away from the screen that has user specific data)
On startup of your app you should call a function like this:
func reactToiCloudloginChanges() {
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSUbiquityIdentityDidChange, object: nil, queue: nil) { _ in
// The user’s iCloud login changed: should refresh all local user data here.
Async.main {
self.viewController?.removeFromParentViewController()
// Or some other code to go back to your start screen.
}
return
}
}

iOS game center -- frequent CONNECTION INTERRUPTED messages

I am working on an iOS game center game, using GKTurnBasedMatch. Every time an incomplete turn, there is a message in the console like this:
2013-04-26 19:26:45.115 AppName[6439:5a9f] CONNECTION INTERRUPTED
Interestingly, this does not happen when I send a complete turn with
[match endTurnWithNextParticipants: nextParticipants turnTimeout:100000 matchData: data completionHandler:^(NSError* error){
// some block here
}];
but it does happen when I send an incomplete turn with
[match saveCurrentTurnWithMatchData:data completionHandler:^(NSError* error) {
// some block here
}];
Someone else reported a similar problem here: Spurious Game Center player disconnect messages. However, it is difficult to see how the only answer there applies to my situation, as I am creating my matches with GKMatchmakerViewController.
i am having the exact same issue with saveCurrentTurnWithMatchData
infact sometimes i have seen the GameData is not updated with saveCurrentTurnWithMatchData while it returns no error

game center sandbox -- neither truly logged in nor out

I am working on a new version of my app. I was using the sandbox normally for a while, but now all of my devices are stuck with a very strange problem. They appear to be halfway logged into game center. It doesn't work for them, but they can't log out either. Here is my authentication method:
- (void)authenticateLocalPlayer {
GKLocalPlayer* localPlayer = WJLocalPlayer;
WJLog(#"Authenticating local user...");
if (localPlayer.authenticated == NO) {
localPlayer.authenticateHandler = ^ (UIViewController* vc, NSError *error) {
if (error) {
WJLog(#"Authentication failed! %#", [error localizedDescription]);
}
else {
WJLog(#"Authentication succeeded!");
NSString* name = [GKLocalPlayer localPlayer].displayName;
WJLog(#"display name is %#", name);
NSString* alias = [GKLocalPlayer localPlayer].alias;
WJLog(#"alias is %#", alias);
GKTurnBasedEventHandler *ev = [GKTurnBasedEventHandler sharedTurnBasedEventHandler];
ev.delegate = self;
}
};
}
}
And here is what I am seeing from the log statements [WJLog is just my own version of NSLog without the garbage]:
Authenticating local user...
Authentication succeeded!
display name is Me
alias is (null)
I can log in or out in the game center app. It makes no difference. I always see the above. I even tried restoring one of the devices to factory settings. The result was still the same. I also tried disabling and re-enabling game center for the new version of my app. Still the same result.
Any ideas?
You're completely ignoring the UIViewController parameter. You're supposed to present this to the user if it exists, so they can log in. Probably you are only now experiencing this because you logged in to the non-sandbox game center, and now when you run the app it wants to ask you for your sandbox credentials, but instead you're assuming you're authenticated.
You have some other problems here too:
You should set the authenticateHandler once only, soon after your app launches.
You should check localPlayer.authenticated inside your authenticateHandler, and nowhere else, as this is the only place it's guaranteed to be valid. Specifically, it's a meaningless value after you resume from the background and until your authenticateHandler gets called again. If you need it elsewhere, use a global variable that gets initialised to false at startup and also in your applicationWillEnterForeground method, and only gets set to true inside your authenticateHandler when you've determined that the localPlayer is actually authenticated.
Check the error and log it by all means, but it doesn't tell you anything about whether authentication actually succeeded, so remove the 'else'.
Have a look at the documentation here.

Game Center score reporting problems

I am developing an app that reports a score to Game Center using the code below (as suggested by Apple).
My problem is that even when my iPhone is in Airplane mode, the app does not trigger any score reporting error. It just goes to the "Submission ok" section of the code.
Any idea why?
Thank you!
GKScore *scoreReporter = [[[GKScore alloc] initWithCategory:category] autorelease];
scoreReporter.value = score;
[scoreReporter reportScoreWithCompletionHandler:^(NSError *error) {
if (error != nil)
{
// handle the reporting error
NSLog(#"Error Descr %#",error.localizedDescription);
NSLog(#"Error Code %#",error.code);
NSLog(#"Error Domain %#",error.domain);
}
else {
NSLog(#"Submission ok");
}
}];
Starting with iOS 5.0, any network errors arising out of reportScoreWithCompletionHandler are handled internally by GameKit. This means that developers no longer have to worry about resubmitting scores pending due to network failures. If you're building with iOS 5.0 and later, the completion handler of reportScoreWithCompletionHandler will not receive any network-related errors.
I would suggest using Apple's reachability flags to detect an active connection yourself. If a connection isn't available, store your Game Center requests for future submission and submit them when network becomes available again. More on reachability can be found here

Resources