According to the Apple docs we should do something like this to handle GC authentication:
- (void) authenticateLocalUser
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
if(localPlayer.authenticated == NO)
{
[localPlayer setAuthenticateHandler:(^(UIViewController* viewcontroller, NSError *error) {
if (!error && viewcontroller)
{
DLog(#"Need to log in");
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[appDelegate.window.rootViewController presentViewController:viewcontroller animated:YES completion:nil];
}
else
{
DLog(#"Success");
}
})];
}
}
And we are given this information:
If the device does not have an authenticated player, Game Kit passes a view controller to your authenticate handler. When presented, this view controller displays the authentication user interface. Your game should pause other activities that require user interaction (such as your game loop), present this view controller and then return. When the player finishes interacting with it, the view controller is dismissed automatically.
My question is, how do we know when this view controller gets dismissed, and how do we know if the authentication succeeded or not?
Obviously I need to know if the authentication worked or not, and I need to know when to resume the game if I had to pause it because the magic GC view controller was presented.
There is a problem with your code: First and foremost, you should set the authentication handler as soon as your app loads. This means that regardless of whether the localPlayer is authenticated or not, you set the handler so that it is automatically called if the player is logged out and logged back in again. If your player switches from your app to the game center app, and logs out / in, then the handler in your app won't be called (if he was already authenticated when the app first started up). The point of setting the handler is so that every time there is an auth change (in / out), your app can do the right thing.
Secondly, you shouldn't be relying on the error for anything. Even if an error is returned, game kit may still have enough cached information to provide an authenticated player to your game. The errors are only to assist you with debugging.
To answer your questions, first review my code example below.
-(void)authenticateLocalPlayer
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
//Block is called each time GameKit automatically authenticates
localPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error)
{
[self setLastError:error];
if (viewController)
{
self.authenticationViewController = viewController;
[self disableGameCenter];
}
else if (localPlayer.isAuthenticated)
{
[self authenticatedPlayer];
}
else
{
[self disableGameCenter];
}
};
}
-(void)authenticatedPlayer
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
[[NSNotificationCenter defaultCenter]postNotificationName:AUTHENTICATED_NOTIFICATION object:nil];
NSLog(#"Local player:%# authenticated into game center",localPlayer.playerID);
}
-(void)disableGameCenter
{
//A notification so that every observer responds appropriately to disable game center features
[[NSNotificationCenter defaultCenter]postNotificationName:UNAUTHENTICATED_NOTIFICATION object:nil];
NSLog(#"Disabled game center");
}
In my app, the call to authenticateLocalPlayer is made only once, when the app is launched. This is because the handler is invoked automatically after that.
how do we know when this view controller gets dismissed,
You won't know when this view controller gets dismissed.
The code example in the documentation says to show the view controller at the appropriate time. This means that you shouldn't necessarily show the view controller every time that game center isn't able to log in. In fact, you probably shouldn't present it immediately in the handler. You should show the view controller only when it is necessary for your player to proceed with the task at hand. It shouldn't pop up at a weird time. That is why I save the view controller, so I can display later when it makes sense to.
I need to know when to resume the game if I had to pause it because
the magic GC view controller was presented.
If you setup your authentication handler to post notifications based on status changes, you can listen for the event and show a "pause menu" or something, that remains until the user chooses to resume.
how do we know if the authentication succeeded
If the authentication succeeded, then the view controller is nil, and localPlayer.isAuthenticated is true.
or not ?
If authentication failed, then localPlayer.isAuthenticated is false, and the view controller was nil. Authentication failing could have happened for a number of reasons (network etc), and you shouldn't be presenting the view controller in this case, which is why the view controller will be nil.In this scenario, you should disable game center features until the user is next logged in. Since the authentication handler is called automatically, most of the time you shouldn't need to do anything. You can always provide a means to launch the game center app from your app, if you want to prompt the user to do something in game center, which you can't do automatically through your code.
EDIT: using a flag like self.isAuthenticated (as I did above)to keep track of whether you are logged in or not is not a great idea (I didn't want to cause any confusion, so I didn't remove it). It is better to always check [GKLocalPlayer localPlayer].isAuthenticated
EDIT: Cleaned up code a bit - removed unnecessary self.isAuthenticated, and block variable which isn't required.
For some reason, the Game Center authentication view controller is an instance of GKHostedAuthenticateViewController which is a private class we're not allowed to use or reference. It doesn't give us any way to cleanly detect when it is dismissed (unlike instances of GKGameCenterViewController which allow us to via the GKGameCenterControllerDelegate protocol.
This solution (read workaround) works by testing in the background every quarter of a second for when the view controller has been dismissed. It's not pretty, but it works.
The code below should be part of your presentingViewController, which should conform to the GKGameCenterControllerDelegate protocol.
Swift and Objective-C provided.
// Swift
func authenticateLocalUser() {
if GKLocalPlayer.localPlayer().authenticateHandler == nil {
GKLocalPlayer.localPlayer().authenticateHandler = { (gameCenterViewController: UIViewController?, gameCenterError: NSError?) in
if let gameCenterError = gameCenterError {
log.error("Game Center Error: \(gameCenterError.localizedDescription)")
}
if let gameCenterViewControllerToPresent = gameCenterViewController {
self.presentGameCenterController(gameCenterViewControllerToPresent)
}
else if GKLocalPlayer.localPlayer().authenticated {
// Enable GameKit features
log.debug("Player already authenticated")
}
else {
// Disable GameKit features
log.debug("Player not authenticated")
}
}
}
else {
log.debug("Authentication Handler already set")
}
}
func testForGameCenterDismissal() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.25 * Double(NSEC_PER_SEC))), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
if let presentedViewController = self.presentedViewController {
log.debug("Still presenting game center login")
self.testForGameCenterDismissal()
}
else {
log.debug("Done presenting, clean up")
self.gameCenterViewControllerCleanUp()
}
}
}
func presentGameCenterController(viewController: UIViewController) {
var testForGameCenterDismissalInBackground = true
if let gameCenterViewController = viewController as? GKGameCenterViewController {
gameCenterViewController.gameCenterDelegate = self
testForGameCenterDismissalInBackground = false
}
presentViewController(viewController, animated: true) { () -> Void in
if testForGameCenterDismissalInBackground {
self.testForGameCenterDismissal()
}
}
}
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController!) {
gameCenterViewControllerCleanUp()
}
func gameCenterViewControllerCleanUp() {
// Do whatever needs to be done here, resume game etc
}
Note: the log.error and log.debug calls are referencing XCGLogger: https://github.com/DaveWoodCom/XCGLogger
// Objective-C
- (void)authenticateLocalUser
{
GKLocalPlayer* localPlayer = [GKLocalPlayer localPlayer];
__weak __typeof__(self) weakSelf = self;
if (!localPlayer.authenticateHandler) {
[localPlayer setAuthenticateHandler:(^(UIViewController* viewcontroller, NSError* error) {
if (error) {
DLog(#"Game Center Error: %#", [error localizedDescription]);
}
if (viewcontroller) {
[weakSelf presentGameCenterController:viewcontroller];
}
else if ([[GKLocalPlayer localPlayer] isAuthenticated]) {
// Enable GameKit features
DLog(#"Player already authenticated");
}
else {
// Disable GameKit features
DLog(#"Player not authenticated");
}
})];
}
else {
DLog(#"Authentication Handler already set");
}
}
- (void)testForGameCenterDismissal
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
if (self.presentedViewController) {
DLog(#"Still presenting game center login");
[self testForGameCenterDismissal];
}
else {
DLog(#"Done presenting, clean up");
[self gameCenterViewControllerCleanUp];
}
});
}
- (void)presentGameCenterController:(UIViewController*)viewController
{
BOOL testForGameCenterDismissalInBackground = YES;
if ([viewController isKindOfClass:[GKGameCenterViewController class]]) {
[(GKGameCenterViewController*)viewController setGameCenterDelegate:self];
testForGameCenterDismissalInBackground = NO;
}
[self presentViewController:viewController animated:YES completion:^{
if (testForGameCenterDismissalInBackground) {
[self testForGameCenterDismissal];
}
}];
}
- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController*)gameCenterViewController
{
[self gameCenterViewControllerCleanUp];
}
- (void)gameCenterViewControllerCleanUp
{
// Do whatever needs to be done here, resume game etc
}
I might be wrong, but I think there actually is a way to know when the authentication view controller gets dismissed. I believe the initial authenticate handler that you set will be called when the user dismisses the authentication view controller, except this time the viewController parameter of the handler will be nil.
The way my app works is: the authenticate handler is set at the beginning of the application, but the authentication view controller is only displayed when the user asks to view the Leaderboards. Then, when this authentication view controller is dismissed, the initial authenticate handler either displays the leaderboards if the user was authenticated or doesn't if he wasn't.
The Game Center DELEGATE method: 'gameCenterViewControllerDidFinish' is called automatically when the Game Center viewController is 'Done'. (This is a compulsory method for the delegate.)
You can put whatever you need for your app, in this method.
Related
I wish to present a particular view Controller when the user taps on a view controller. I know there are many questions for the same thing that have been posted before and there have been many answers as well but none of it seems to be working for me. Here is the code in the 'didReceiveRemoteNotification' method in my 'AppDelegate'
if(application.applicationState == UIApplicationStateInactive) {
NSLog(#"Inactive");
//Show the view with the content of the push
if(userInfo)
{
NSLog(#"In inactive payload");
// Create a pointer to the C object
NSString *cId = [userInfo objectForKey:#"c"];
NSLog(cId);
PFObject *targetC = [PFObject objectWithoutDataWithClassName:#"C" objectId:cId];
// Fetch C object
[targetC fetchInBackgroundWithBlock:^(PFObject *object, NSError *error) {
C *c = [[C alloc] initWithPFObject:object];
// Show c view controller
if (!error) {
NSLog(c.title);
CDetailsViewController *viewController = [[CDetailsViewController alloc] initWithC:c];
//[self.navigationController pushViewController:viewController animated:YES];
[self.navigationController presentViewController:viewController animated:YES completion:nil];
}
}];
}
handler(UIBackgroundFetchResultNewData);
I am getting the exact data I have sent in my push as I can see from the log prints I get. Only problem is the view controller that I am trying to present/push is never seen when the notification is tapped. Any workaround for this? Any thing that I'm doing wrong? Any help is highly appreciated.
If your app is in background, the method "application:didReceiveRemoteNotification:" is never called.
You need add this, in the method: "application:didFinishLaunchingWithOptions:"
// At the end of the method you force call didNotification:
// Handle any notifications.
if ([launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]!=nil)
{
NSDictionary * aPush =[launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
[self application:application didReceiveRemoteNotification:aPush];
}
return YES;
}
If your app is started due to the user tap a notification, in the launchOptions dict, the notification payload will be attached.
I assume its because didReceiveRemoteNotification: is called on a background thread and presentViewController might need to be called on the main thread.
The below code, forces the view controller to be presented in the main thread asynchronously Note: a block should never be synchronously run on the main thread. To learn more about Grand Central Dispatch, see here.
dispatch_async(dispatch_get_main_queue(), ^
{
CDetailsViewController *viewController = [[CDetailsViewController alloc] initWithC:c];
[self.navigationController presentViewController:viewController animated:YES completion:nil];
}
I am very new to threads so please be gracious.
In the Login View Controller after the user is authenticated, I launch a thread to get the users geolocation every 30 seconds (only want to do this when the user is logged in) and then move to the main view controller of the application which displays the applications main information.
When the user logs out, I want to cancel the thread created to gather the geolocation every 30 seconds.
How do I do this?
Am I approaching this correct? If not, code examples and explanation pleases
Thanks so much!!!!
Loggin View Controller
...
- (IBAction)loginButton:(id)sender {
NSInteger success = 0;
//Check to see if the username or password texfields are empty or email field is in wrong format
if([self validFields]){
//Try to login user
success = [self loginUser];
}
//If successful, go to the MainView
if (success) {
//Start getting users Geolocation in a thread
[NSThread detachNewThreadSelector:#selector(startGeolocation:) toTarget:self withObject:nil];
//Go to Main view controller
[self performSegueWithIdentifier:#"loginSuccessSegue" sender:self];
}
else
{
//Reset password text field
self.passwordTextField.text = #"";
}
}
...
//Thread to get Geolocation every 30 seconds
-(void)startGeolocation:(id)param{
self.geoLocation = [[GeoLocation alloc] init];
while(1)
{
//****************START GEOLOCATION*******************************//
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
[appDelegate.databaseLock lock];
NSLog(#"Geolocation:(%f,%f)", [self.geoLocation getLatitude], [self.geoLocation getLongitude]);
sleep(30);
[appDelegate.databaseLock unlock];
}
}
Main View Controller
...
//When the Logout Button in MenuView is pressed this method will be called
- (void)logoutButton{
//Cancel the geolocation tread
//????????????????????????????
//Log the user out
[self logoutUser]
}
...
I would recommend using GCD for this.
dispatch_queue_t dq = dispatch_queue_create("bkgrndQueue", NULL);
dispatch_async(dq, ^{
#autoreleasepool{
while(SEMAPHORE_NAME){
// do stuff in here
}
}
}
and then in your other view controller
SEMAPHORE_NAME = NO;
Okay I am kind of new to IOS development, but I am writing an application where I am using a timer class to time out the user if they idle too long on any particular scene in my storyboard and it bumps the user back to the original scene/view. I have a single story board that is made up of several scenes/views(not sure what the correct word here is), and each scene has its own view controller.
I accomplish the timeout via the appdelegate class. See code below.
So I have the code working and it works great, but I am trying to make it so that it will ignore the timer if we are on the main scene.
I have googled this, read copious amounts of documentation, and have tried many things but so far I haven't been able to figure out how to get the currently viewed scene in the applicationDidTimeout method.
If I can get the name of the currently viewed scene/view, then I can choose to ignore the timer or not.
Does anyone know how to do this?
Thank you for your time.
#import "StoryboardAppDelegate.h"
#import "TIMERUIApplication.h"
#implementation StoryboardAppDelegate
#synthesize window = _window;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// applicaiton has timed out
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(applicationDidTimeout:) name:kApplicationDidTimeoutNotification object:nil];
return YES;
}
-(void)applicationDidTimeout:(NSNotification *) notif
{
NSLog (#"time exceeded!!");
UIViewController *controller = [[UIStoryboard storyboardWithName:#"Main" bundle:NULL] instantiateViewControllerWithIdentifier:#"StoryboardViewController"];
UINavigationController * navigation = [[UINavigationController alloc]initWithRootViewController:controller];
[self.window setRootViewController:navigation];
navigation.delegate = self;
navigation.navigationBarHidden = YES;
if (controller) {
#try {
[navigation pushViewController:controller animated:NO];
} #catch (NSException * ex) {
//“Pushing the same view controller instance more than once is not supported”
//NSInvalidArgumentException
NSLog(#"Exception: [%#]:%#",[ex class], ex );
NSLog(#"ex.name:'%#'", ex.name);
NSLog(#"ex.reason:'%#'", ex.reason);
//Full error includes class pointer address so only care if it starts with this error
NSRange range = [ex.reason rangeOfString:#"Pushing the same view controller instance more than once is not supported"];
if ([ex.name isEqualToString:#"NSInvalidArgumentException"] &&
range.location != NSNotFound) {
//view controller already exists in the stack - just pop back to it
[navigation popToViewController:controller animated:NO];
} else {
NSLog(#"ERROR:UNHANDLED EXCEPTION TYPE:%#", ex);
}
} #finally {
//NSLog(#"finally");
}
} else {
NSLog(#"ERROR:pushViewController: viewController is nil");
}
[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];
}
#end
I'm assuming you've written the logic for the timer somewhere. Can you just invalidate the timer when you've popped back to the rootViewController?
Also instead of pushing a viewController onto the navigationViewController and handling the errors, you should check to see if the controller you're pushing is already in the stack like so:
if (![navigation.viewControllers containsObject:viewController] {
// push onto the stack
}
You could also check to see how many levels are currently in the navigationController by checking the count of the viewControllers array like so:
if ([navigation.viewControllers count] == 0) {
// I know we're on the main screen because no additional viewControllers have been added to the stack.
}
If you are not using modal controllers anywhere then the simplest solution would be
UINavigationController* nav = (UINavigationController*)self.window.rootViewController; // You could just save the nav as part of your app delegate
if (nav.viewControllers.count > 1){
[nav popToRootViewControllerAnimated:YES];
}
This is different then your current code because your main page will not be deleted and recreated every time the timer goes off
Okay I figured out how to do this. I was making this way too complicated.
To solve this I simply made a property and method in the app delegate class where I could set a scene name.
Then in each view controller header file I import the header file for the app delegate class and define a reference to it. Then in the load event for each view I simply set the scene name in the app delegate class using this line of code.
[myAppDelegate setSceneName:self.title];
Easy peasy!
I'm using Apples Game Center to sign the players in at the start of the game, the problem is I've just hit cancel (to test the eventually) and now the dialogue box won't appear anymore, it just keeps going straight through to disabled.
Here's the function I'm using.
-(void) setup
{
gameCenterAuthenticationComplete = NO;
if (!isGameCenterAPIAvailable()) {
// Game Center is not available.
NSLog(#"Game Center is not available.");
} else {
NSLog(#"Game Center is available.");
__weak typeof(self) weakSelf = self; // removes retain cycle error
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer]; // localPlayer is the public GKLocalPlayer
__weak GKLocalPlayer *weakPlayer = localPlayer; // removes retain cycle error
weakPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error)
{
if (viewController != nil)
{
NSLog(#"Try to show ViewController");
[weakSelf showAuthenticationDialogWhenReasonable:viewController];
}
else if (weakPlayer.isAuthenticated)
{
NSLog(#"authenticate player");
[weakSelf authenticatedPlayer:weakPlayer];
}
else
{
NSLog(#"disable");
[weakSelf disableGameCenter];
}
};
}
}
-(void)disableGameCenter
{
}
As you can see disableGameCenter doesn't actually do anything anyway.
Why is it going to disable every time I now run it? (working with the simulator) and how can I get it out of that so the the dialogue appears again? Do I have to manually force the dialogue to appear again somehow?
I had a similar issue under OSX, after you "cancel" the Gamecenter dialog a few times, it stops appearing.
To get it back, run "Game Center", Login and Logout - and then run your game again and the dialogbox will appear again ( it works at least a few times, until you 'cancel' too often ).
How can I use Game Center or the GameKit Framework with a Sprite Kit Xcode template?
In Sprite kit, it uses Scenes; but normally to view the leaderboards for example you need to "presentModalViewController" but that is not possible in SKView.
And how can I authenticate the player and all that other fun stuff in iOS 6.
Thanks in advance!
You can use "presentModalViewController" by using this code to access the root view controller
UIViewController *vc = self.view.window.rootViewController;
[vc presentViewController: gameCenterController animated: YES completion:nil];
Now you can access your ModelViewController anywhere include in SKScenes. I did it in my newest game and it worked well
Besides, I suggest you use the separate object to control game center like leaderboard and achievement so you can reuse it in your next game.
Here is an updated authenticate local player, but Ravindra's code also works.
- (void) authenticateLocalPlayer
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
localPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error){
if (viewController != nil)
{
//showAuthenticationDialogWhenReasonable: is an example method name. Create your own method that displays an authentication view when appropriate for your app.
//[self showAuthenticationDialogWhenReasonable: viewController];
}
else if (localPlayer.isAuthenticated)
{
//authenticatedPlayer: is an example method name. Create your own method that is called after the loacal player is authenticated.
//[self authenticatedPlayer: localPlayer];
}
else
{
//[self disableGameCenter];
}
};
}
Swift 2.0
func authenticateLocalPlayer() {
let localPlayer = GKLocalPlayer.localPlayer()
localPlayer.authenticateHandler = { (viewController, error ) -> Void in
if (viewController != nil) {
let vc:UIViewController = self.view!.window!.rootViewController!
vc.presentViewController(viewController!, animated: true, completion:nil)
} else {
print ("Authentication is \(GKLocalPlayer.localPlayer().authenticated) ")
GlobalData.loggedIntoGC = true
// do something based on the player being logged in.
GlobalData Swift File:
static var loggedIntoGC:Bool = false
Call Method in your scene where Game Center is being enabled:
ie HUD or GameScene in the
override func didMoveToView(view: SKView)`
authenticateLocalPlayer()
you can authenticate like this
[[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:^(NSError *error) {
if (error == nil)
{
static_setEnable( true );
NSLog(#" Authenticate local player complete");
}
else
{
static_setEnable( false );
NSLog(#"Authenticate local player Error: %#", [error description]);
}
}];
}