2
votes

Currently, my app prompts a user to log into Game Center as soon as they open the app. Once logged in, they can view their achievements and leaderboards. However; if the user declines to log into game center and then presses the leaderboard or achievement button, the entire app crashes. What the app should do in this situation is re-prompt the user to log in. Any suggestions would be greatly appreciated.

class viewController: UIViewController, GKGameCenterControllerDelegate {

    var highscore = NSUserDefaults.standardUserDefaults().integerForKey("highscore")
    var loggedin = 1

    override func viewDidLoad() {
        super.viewDidLoad()
        login()
    }

    func login() {
        println("Game Center Login Called")
        let localPlayer = GKLocalPlayer.localPlayer()
        loggedin = 2

        // Handle the authentication
        localPlayer.authenticateHandler = {(Home: UIViewController!, error: NSError!) -> Void in
            if Home != nil {
                println("Authentication is being processed.")
                self.presentViewController(Home, animated: true, completion: nil)

            } else {
                println("Player has been successfully authenticated.")
            }
        }
    }

    func showLeaderboard() {
        let gkScore = GKScore(leaderboardIdentifier: "high_Score_Leader_Board")
        gkScore.value = Int64(highscore)
        GKScore.reportScores([gkScore], withCompletionHandler: ( { (error: NSError!) -> Void in
            if (error != nil) {
                // handle error
                println("Error: " + error.localizedDescription);
            } else {
                println("Score reported: \(gkScore.value)")
            }
        }))

        var gcViewController: GKGameCenterViewController = GKGameCenterViewController()
        gcViewController.gameCenterDelegate = self

        gcViewController.viewState = GKGameCenterViewControllerState.Leaderboards

        gcViewController.leaderboardIdentifier = "high_Score_Leader_Board"
        self.showViewController(gcViewController, sender: self)
        self.presentViewController(gcViewController, animated: true, completion: nil)
    }

    @IBAction func gameCenterButtoPressed(sender: AnyObject) {
        if loggedin == 2 {
            showLeaderboard()

        }

        else {
            login()
        }
    }

    @IBAction func pointButtonScored(sender: AnyObject) {
       ReportAchievment("testbutton", percentComplete: 100)
    }

    func gameCenterViewControllerDidFinish(gcViewController: GKGameCenterViewController!)
    {
        self.dismissViewControllerAnimated(true, completion: nil)
    }

    func showAchievements() {
        var gcViewController: GKGameCenterViewController = GKGameCenterViewController()
        gcViewController.gameCenterDelegate = self

        gcViewController.viewState = GKGameCenterViewControllerState.Achievements

        self.showViewController(gcViewController, sender: self)
        self.presentViewController(gcViewController, animated: true, completion: nil)
    }

    func ReportAchievment(identifier : String, percentComplete : Double)
    {
        var achievement = GKAchievement(identifier: identifier)

        if(achievement != nil)
        {
            achievement.percentComplete = percentComplete;
            achievement.showsCompletionBanner = true

            GKAchievement.reportAchievements([achievement], withCompletionHandler: { (error : NSError!) -> Void in
                println("Achievement reported")
            })
        }
    }

    @IBAction func achievementButtonPressed(sender: AnyObject) {

       showAchievements()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
2

2 Answers

4
votes

This is the code that I use to authenticate the player for Game Center. You can call the checkIfPlayerIsLoggedIn() function the moment your game loads, which will check to see if the player is already logged in. If they player isn't, it will move to the loginPlayer() function, which attempts to log the player in if Game Center has already been setup, and if not, it will display the Game Center login screen. If the player chooses not to log in, it will return an error, which from there you can decide how to handle it (which in your case would probably be telling the user that they have to sign in and attempting to display the view controller again)

let localPlayer = GKLocalPlayer.localPlayer()
var delegate: GameCenterInteractorNotifications?
var callingViewController: UIViewController?

func checkIfPlayerIsLoggedIn()
{
    if (self.localPlayer.authenticated == false)
    {
        self.loginPlayer()
    } else
    {
        self.localPlayer.registerListener(self)
        // Do achievement loading and matchmaking handling here
    }
}

private func loginPlayer()
{
    self.delegate?.willSignIn()

    self.localPlayer.authenticateHandler = {(viewController : UIViewController!, error : NSError!) -> Void in

        if (viewController != nil)
        {
            dispatch_async(dispatch_get_main_queue(), {
                self.presentViewController(viewController, animated: true, completion: nil)
            })
        }

        else if (self.localPlayer.authenticated == true) // Player signed in
        {
            self.localPlayer.registerListener(self)
            self.delegate?.didSignIn()
        }

        else // Player did not sign in
        {
            self.delegate?.failedToSignIn()
        }

        if (error != nil)
        {
            println("Failed to sign in with error:\(error.localizedDescription).")
            self.delegate?.failedToSignInWithError(error)
            // Add code to determine what to do with the error
        }
    }
}
3
votes

Zachary posted a good example of an authenticateHandler. There are some additional observations I will tack on to his post:

  1. Like Zachary does, always check the NSError passed into the handler. If the Error is set, then the VC will be nil. So, in the OP's original code, which only checks if the VC is set, an ERROR + NULL VC will fall through a path assuming the user is logged in when they really aren't.

  2. When checking the NSError, error.code ==2 means the user canceled the login. In the OP's case, they may want to check specifically for error.code == 2, then disable the parts of the game that won't work (as well as give the user some instruction on what to do to fix it).

  3. Speaking of "fixing it," as of IOS8.4, there is no way to re-show the login VC. If they canceled it, or if game center randomly de-authorized them (which happens), the only thing the user can do is kill the game and restart it completely. That will initiate a new login sequence.

  4. Canceling the sandbox login 3 times permanently disables sandbox access, requiring a full reset on the device in question. Hence, it's not a bad idea to count (and save) the number of times you see error.code == 2 appear, and warn the user when they're about to lock out their machine. (No longer relevant since the sandbox went away with IOS9)

  5. Lastly, there is at least one bug where the error is set, but localPlayer.authenticated will still report YES. I worked until 3am one night trying to figure that one out. It's not consistent, and seems more associated with error.code == -1001 (when in airplane mode, or total network loss). I have a bug open with Apple on this one. So the moral is: always check the NSerror passed to your authenticateHandler regardless of what localPlayer.authenticated says.

Update #5: Apple closed my bug on #5, saying this is working as intended. Apple seems to loathe ever telling the user something wrong... to the extent that you'll be shown as "logged in" using previously cached credentials and showing previously cached leaderboards, when you really have no connection at all to game center. In my opinion, this just frustrates and confuses users that don't understand how they can be logged in, but can't actually play a game.