I'm using Game Center in my app and I'm having some problem with the GKLocalPlayer.authenticated attribute. Regardless if the authentication process is successful or not, localPlayer.authenticated always returns false. This also happens if my device is already logged in to Game Center.
I get this both on actual device (iPhone 6s) and simulator (tried several).
The only information I've found about this suggests that there is a problem with the time settings but they seem to be fine.
Is this a bug or am I doing something wrong?
private static let localPlayer = GKLocalPlayer()
static func authenticateLocalPlayer() {
localPlayer.authenticateHandler = { (viewController, error) -> Void in
if let viewController = viewController {
if let rootViewController = UIApplication.sharedApplication().keyWindow?.rootViewController {
rootViewController.presentViewController(viewController, animated: true, completion: nil)
}
} else if localPlayer.authenticated {
gameCenterEnabled = true
let defaultCenter = NSNotificationCenter.defaultCenter()
defaultCenter.postNotificationName("local_player_authenticated", object: nil)
} else {
gameCenterEnabled = false
}
if let error = error {
print(error)
}
}
}
static func isAuthenticated() -> Bool {
return localPlayer.authenticated
}
My bad, looks like I made a little mistake when translating this code from Objective-C. It's supposed to be
GKLocalPlayer.localPlayer()
not
GKLocalPlayer()
Related
In objC the syntax written by Rawendrich for GKTurnBasedEventListener, which was GKTurnBasedEventHandler there at that time, now changed by Apple is as below.
if (!gameCenterAvailable) return;
void (^setGKEventHandlerDelegate)(NSError *) = ^ (NSError *error)
{
GKTurnBasedEventHandler *ev =
[GKTurnBasedEventHandler sharedTurnBasedEventHandler];
ev.delegate = self;
};
NSLog(#"Authenticating local user...");
if ([GKLocalPlayer localPlayer].authenticated == NO) {
[[GKLocalPlayer localPlayer]
authenticateWithCompletionHandler:
setGKEventHandlerDelegate];
} else {
NSLog(#"Already authenticated!");
setGKEventHandlerDelegate(nil);
}
Now after converting this to swift, and with the composition of writing down GKTurnBasedEventListener instead of GKTurnBasedEventHandler, this comes the following way.
// Converted with Swiftify v1.0.6381 - https://objectivec2swift.com/
if !gameCenterAvailable {
return
}
var setGKEventHandlerDelegate: ((_: Error) -> Void)? = {(_ error: Error?) -> Void in
var ev = GKTurnBasedEventHandler.shared()
ev.delegate = self
}
print("Authenticating local user...")
if GKLocalPlayer.localPlayer().authenticated == false {
GKLocalPlayer.localPlayer().authenticate(withCompletionHandler: setGKEventHandlerDelegate)
}
else {
print("Already authenticated!")
setGKEventHandlerDelegate(nil)
}
Unfortunately this is not the right syntax to set delegate of GKTurnBasedEventListener for my ViewController.
Please if anyone of you could solve this for me, because without this I'm not able to read through event listener's default functions.
Cheers!
FYI, if you want a working example of how to use GKLocalPlayerListener during a turn-based GameKit match, you are welcome to take a look at this example project for a turn based game. I hope it helps to see all the pieces in context.
Finally after just about harsh 10 hours, I figured out this problem from Here. Although this syntax is in objC, but there's no problem of converting it to swift from Swiftify.
Although a bit later than the real time, but I'm now able to understand that setting delegate of GKTunBasedEventListener is not like the one we do for UITableViewControllerDelegate.
Here, one must have to first authenticate local player, then after you have to register local player's listener to the ViewController's delegate GKLocalPlayerListener.
One other thing I found on Apple's Documentation:
Do not implement GKChallengeListener, GKInviteEventListener, GKSavedGameListener, and GKTurnBasedEventListener directly; implement GKLocalPlayerListener instead. You can listen for and handle multiple events using GKLocalPlayerListener.
So then on I've implemented in the following way.
import GameKit
class ViewController: UIViewController, GKTurnBasedMatchmakerViewControllerDelegate,
GKLocalPlayerListener {
.....
func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool) {
print("#1")
print(player)
print("#2")
print(match)
print("#3")
print(didBecomeActive)
if match.status == GKTurnBasedMatchStatus.open
{
if GKLocalPlayer.localPlayer() == match.currentParticipant
{
if didBecomeActive
{
// Active now
}
else
{
// Active already
}
}
else
{
// It's someone's turn
if match.matchData != myMatch?.matchData
{
// Match Data being Updated by Someone
print(player.alias ?? "No Name:")
}
}
}
thirdTopLabel.text = match.matchID! + "\n" + didBecomeActive.description
}
....
Now in ViewDidLoad() function put the following code.
// In the ViewDidLoad function
if(!GKLocalPlayer.localPlayer().isAuthenticated)
{
authenticatePlayer { (auth) in
weak var weakSelf = self
weak var weakPlayer = GKLocalPlayer.localPlayer()
if(auth){
weakPlayer?.register(weakSelf!)
self.suthentication = true;
}
else{
print("failed in authentication")
self.suthentication = false;
}
}
}
else
{
// Already Authenticated
GKLocalPlayer.localPlayer().register(self)
localPlayer = GKLocalPlayer.localPlayer()
}
And finally your Authentication function should be like this.
// authenticate local player :: Just Authentication
func authenticatePlayer(completionHandler: #escaping (_ resultedPlaces: Bool) -> Void) {
localPlayer = GKLocalPlayer.localPlayer()
localPlayer.authenticateHandler =
{ (viewController , error ) -> Void in
if viewController != nil
{
self.present(viewController!, animated:true, completion: nil)
}
else
{
if self.localPlayer.isAuthenticated
{
completionHandler(true);
}
else
{
completionHandler(false);
print("not able to authenticate fail")
self.gameCenterEnabled = false
if (error != nil)
{
print("\(error.debugDescription)")
}
else
{
print( "error is nil")
}
}
}
}
}
NOTE: GKLocalPlayerListener won't work on simulator.
I've tried a whole bunch of ways to get Game Center working in my SpriteKit game. Unfortunately the way I've done it in the past using ObjC and ViewControllers don't work because I'm using SKScene/ a GameScene.
This is the swift version of the code (I think):
// MARK: Game Center Integration
//login and make available
func authenticateLocalPlayer(){
let localPlayer = GKLocalPlayer()
print(localPlayer)
localPlayer.authenticateHandler = {(viewController, error) -> Void in
if ((viewController) != nil) {
self.presentViewController(viewController!, animated: true, completion: nil)
}else{
print((GKLocalPlayer.localPlayer().authenticated))
}
}
}
//submit a score to leaderboard
func reportScoreToLeaderboard(thisScore:Int){
if GKLocalPlayer.localPlayer().authenticated {
let scoreReporter = GKScore(leaderboardIdentifier: "LeaderboardID")
scoreReporter.value = Int64(thisScore)
let scoreArray: [GKScore] = [scoreReporter]
GKScore.reportScores(scoreArray, withCompletionHandler: { (error: NSError?) -> Void in
if error != nil {
print(error!.localizedDescription)
} else {
print("Score submitted")
}
})
}
}
//show leaderboard (call from button or touch)
func showLeaderboard() {
let vc = self.view?.window?.rootViewController
let gc = GKGameCenterViewController()
gc.gameCenterDelegate = self
vc?.presentViewController(gc, animated: true, completion: nil)
}
//hides view when finished
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController){
gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
}
Unfortunetely, no matter what I try, I either get this error:
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior
... or it just crashes.
I've read that NSNotifications can be used? But how?
I'm guessing the best way is to set it all up in the GameViewController.swift and use NSNotifications to communicate with the RootViewController from the GameScene? I can't seem to find a tutorial or example though.
Use delegation when a view controller needs to change views, do not have the view itself change views, this could cause the view trying to deallocating while the new view is being presented, thus the error you are getting
I have two UIViewControllers, MainViewController and HighScoreViewController.
In the MainViewController (the initial view controller that the user sees upon using the app) I have all the methods to log into Game Center and save the high score to a leaderboard.
override func viewDidLoad() {
super.viewDidLoad()
authenticateLocalPlayer()
if totalHighScore > prevTotalHighScore {
saveHighScore("totalHighScore", score: totalHighScore)
prevTotalHighScore = totalHighScore
NSUserDefaults.standardUserDefaults().setObject(prevTotalHighScore, forKey: "prevtotalhighscore")
}
}
func authenticateLocalPlayer() {
let localPlayer: GKLocalPlayer = GKLocalPlayer.localPlayer()
localPlayer.authenticateHandler = {(ViewController, error) -> Void in
if((ViewController) != nil) {
// 1 Show login if player is not logged in
self.presentViewController(ViewController!, animated: true, completion: nil)
}
else {
print("Authentication is \(GKLocalPlayer.localPlayer().authenticated)")
}
}
}
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController) {
gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
}
func saveHighScore(identifier: String, score: Int) {
if GKLocalPlayer.localPlayer().authenticated {
let scoreReport = GKScore(leaderboardIdentifier: identifier)
scoreReport.value = Int64(score)
let scoreArray: [GKScore] = [scoreReport]
GKScore.reportScores(scoreArray, withCompletionHandler: { (error) -> Void in
if error != nil {
print(error)
}
else {
print("Posted score of \(score)")
}
})
}
}
First of all, is this the best way to implement as such?
Secondly, my HighScoreViewController has a button that says 'LEADERBOARD' where if the user taps on, Game Center leaderboard for my game would pop up. How do I go about implementing said button? I already have a button set up and and #IBAction method linked to it, but I have no idea what code to place in it since all of the main Game Center code is placed inside the MainViewController.
Once you're authenticated with Game Center, it doesn't matter which controller retrieves the leaderboard. You say you already have a method linked to your button. So, you'll just add the retrieval code there. Apple's GKLeaderBoard reference has an example in obj-c for downloading leaderboard data:
GKLeaderboard *leaderboardRequest = [[GKLeaderboard alloc] init];
if (leaderboardRequest != nil)
{
leaderboardRequest.playerScope = GKLeaderboardPlayerScopeGlobal;
leaderboardRequest.timeScope = GKLeaderboardTimeScopeToday;
leaderboardRequest.identifier = #" ~~ your leaderboard identifier goes here ~~ "
leaderboardRequest.range = NSMakeRange(1,10);
[leaderboardRequest loadScoresWithCompletionHandler: ^(NSArray *scores, NSError *error)
{
if (error != nil)
{
// Handle the error.
}
if (scores != nil)
{
// Process the score information.
}
}];
}
I'm implementing the login possibility with touchID using Swift.
Following: when the App is started, there is a login screen and a touchID popup - that's working fine. The problem occurs, when the app is loaded from background: I want the touchID popup appear over a login screen if a specific timespan hasn't been exceeded yet - but this time I want the touchID to go to the last shown view before the app entered background. (i.e. if the user wants to cancel the touchID, there is a login screen underneath where he then can authenticate via password, which leads him to the last shown view OR if the touchID authentication succeeded, the login screen should be dismissed and the last shown view presented.)
I really tried everything on my own, and searched for answers - nothing did help me. Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
//notify when foreground or background have been entered -> in that case there are two methods that will be invoked: willEnterForeground and didEnterBackground
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "willEnterForeground", name:UIApplicationWillEnterForegroundNotification, object: nil)
notificationCenter.addObserver(self, selector: "didEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
password.secureTextEntry = true
if (username != nil) {
username.text = "bucketFit"
}
username.delegate = self
password.delegate = self
if let alreadyShown : AnyObject? = def.objectForKey("alreadyShown") {
if (alreadyShown == nil){
authenticateWithTouchID()
}
}
}
willEnterForeground:
func willEnterForeground() {
//save locally that the guide already logged in once and the application is just entering foreground
//the variable alreadyShown is used for presenting the touchID, see viewDidAppear method
def.setObject(true, forKey: "alreadyShown")
if let backgroundEntered : AnyObject? = def.objectForKey("backgroundEntered") {
let startTime = backgroundEntered as! NSDate
//number of seconds the app was in the background
let inactivityDuration = NSDate().timeIntervalSinceDate(startTime)
//if the app was longer than 3 minutes inactiv, ask the guide to input his password
if (inactivityDuration > 2) {
showLoginView()
} else {
def.removeObjectForKey("alreadyShown")
showLoginView()
}
}
}
authenticateWithTouchID():
func authenticateWithTouchID() {
let context : LAContext = LAContext()
context.localizedFallbackTitle = ""
var error : NSError?
let myLocalizedReasonString : NSString = "Authentication is required"
//check whether the iphone has the touchID possibility at all
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) {
//if yes then execute the touchID and see whether the finger print matches
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: myLocalizedReasonString as String, reply: { (success : Bool, evaluationError : NSError?) -> Void in
//touchID succeded -> go to students list page
if success {
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.performSegueWithIdentifier("studentsList", sender: self)
})
} else {
// Authentification failed
print(evaluationError?.description)
//print out the specific error
switch evaluationError!.code {
case LAError.SystemCancel.rawValue:
print("Authentication cancelled by the system")
case LAError.UserCancel.rawValue:
print("Authentication cancelled by the user")
default:
print("Authentication failed")
}
}
})
}
}
shouldPerformSegueWithIdentifier:
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
if (false) { //TODO -> username.text!.isEmpty || password.text!.isEmpty
notify("Login failed", message: "Please enter your username and password to proceed")
return false
} else if (false) { //TODO when backend ready! -> !login("bucketFit", password: "test")
notify("Incorrect username or password", message: "Please try again")
return false
//if the login page is loaded after background, dont proceed (then we need to present the last presented view on the stack before the app leaved to background)
} else if let alreadyShown : AnyObject? = def.objectForKey("alreadyShown") {
if (alreadyShown != nil){
//TODO check whether login data is correct
dismissLoginView()
return false
}
}
return true
}
Thank you in advance.
What you could do is create a AuthenticationManager. This manager would be a shared instance which keep track of whether authentication needs to be renewed. You may also want this to contain all of the auth methods.
class AuthenticationManager {
static let sharedInstance = AuthenticationManager()
var needsAuthentication = false
}
In AppDelegate:
func willEnterForeground() {
def.setObject(true, forKey: "alreadyShown")
if let backgroundEntered : AnyObject? = def.objectForKey("backgroundEntered") {
let startTime = backgroundEntered as! NSDate
//number of seconds the app was in the background
let inactivityDuration = NSDate().timeIntervalSinceDate(startTime)
//if the app was longer than 3 minutes inactiv, ask the guide to input his password
if (inactivityDuration > 2) {
AuthenticationManager.sharedInstance.needsAuthentication = true
}
}
}
Then, subclass UIViewController with a view controller named SecureViewController. Override viewDidLoad() in this subclass
override fun viewDidLoad() {
super.viewDidLoad()
if (AuthenticationManager.sharedInstance().needsAuthentication) {
// call authentication methods
}
}
Now, make all your View Controllers that require authentication subclasses of SecureViewController.
I've tried everything, but the only way I could get a successful test is to actually send the notification in the test function, which kinda defeats the purpose.
I have a button. When I tap the button, it sends a notification. How can I use expectationForNotification to see if this notification gets sent?
func testExample() {
let app = XCUIApplication()
let button = app.buttons["Button"]
let expectation = expectationForNotification("TEST_NOTE", object: nil) {
(notification: NSNotification!) -> Bool in
print("SUCCESS")
return true
}
button.tap()
waitForExpectationsWithTimeout(5, handler: nil)
}
it looks to me that you will have to fulfill the expectation...
func testExample()
{
let app = XCUIApplication()
let button = app.buttons["Button"]
let expectation = expectationWithDescription("waiting for the tap")
expectationForNotification("TEST_NOTE", object: nil)
{
notification in
expectation.fulfill()
return true
}
button.tap()
waitForExpectationsWithTimeout(30)
{
error in
if let e = error
{
XCTFail("\(e.debugDescription)")
}
}
}