How can I tell if the GKHostedAuthenticateViewController is gone?
I'm trying to detect when an Apple GameKit user identity is rejected and sent to a Cocos2D app. I want to show a different view right after the user is authenticated, so I am just trying to find the corresponding trigger.
Here's what I do: Following the Apple Game Center Programming Guide, I check [[GKLocalPlayer localPlayer] isAuthenticated]
, and if the user is not working, I set the AuthenticationHandler and save the returned viewController like this:
[GKLocalPlayer localPlayer].authenticateHandler = ^(UIViewController *viewController, NSError *error){
if (viewController != nil)
{
//save the returned view controller to show it when user tries to access leaderboards, etc.
_savedGCAuthViewController = viewController;
} else if([[GKLocalPlayer localPlayer] isAuthenticated]){
_userAuthenticated = YES;
}
};
I keep the returned viewController so that it can be displayed at a more appropriate time, for example:
AppController *appDelegate = (AppController *)[[UIApplication sharedApplication] delegate];
[appDelegate.navController presentViewController:_savedGCAuthViewController animated:YES completion:nil];
Everything works perfectly. The problem is, I don't know when this opinion went away.
All the other questions / answers I see here on StackOverflow say that the viewController subclass in a situation like this will apply the viewDidDisappear method, but I cannot subclass the view controller because Apple does not provide access to the headers for the GKHostedAuthenticateViewController class.
I've also dug all the messages for notifications, but it doesn't look like the UIViewController is using any messages of type NSNotificationCenter.
Ideas?
I'm relatively new to iOS, but is there a way to somehow get into the view hierarchy and insert my own view that gets removed when the GKHostedAuthenticateViewController does? Maybe as a parent view controller or child?
Thank!
source to share
You can wrap GKHostedAuthenticateViewController
in your own controller before exposing it so you can access the viewDidDisappear / viewWillDisappear methods:
// Simple class to wrap GKHostedAuthenticateViewController
class AuthController: UINavigationController {
var viewWillDisappearHandler: ((Bool) -> ())?
convenience init(authController: UIViewController) {
self.init(rootViewController: authController)
self.modalInPopover = true
self.modalPresentationStyle = UIModalPresentationStyle.FormSheet
self.navigationBarHidden = true
self.preferredContentSize = authController.preferredContentSize
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
viewWillDisappearHandler?(animated)
}
}
Then in yours, GKLocalPlayer.authenticateHandler
you can customize your viewWillDisappearHandler:
GKLocalPlayer.localPlayer().authenticateHandler = {
(viewController: UIViewController?, error: NSError?) -> Void in
if let viewController = viewController {
let authController = AuthController(authController: viewController)
authController.viewWillDisappearHandler = { (animated: Bool) -> () in
println("viewController is disappearing")
}
self.presentViewController(authController, animated: true, completion: nil)
} else {
// Authenticated
}
}
source to share
For some reason, the Game Center Authentication Controller is an instance GKHostedAuthenticateViewController
, which is a private class that we are not allowed to use or reference. This does not give us any way to cleanly detect when it is rejected (unlike GKGameCenterViewController
which allows us to use the GKGameCenterControllerDelegate
.
This solution (read workaround) works by testing in the background every quarter of a second when the view controller has been fired. It's not pretty, but it works.
The code below should be part of your presentingViewController, which should be protocol compliant GKGameCenterControllerDelegate
.
Swift and Objective-C.
// 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: calls to log.error and log.debug refer to 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
}
source to share
try KVO. Observe isBeingDismissed
propertyGKHostedAuthenticateViewController
Swift 4 introduced a Key-Path types
new way Key-Path Expression
to create them and a new observe
closure-based feature available for classes that inherit from NSObject.
//invalidate old observation before adding new
gameCenterLoginVCDismissObservation?.invalidate()
//observe when GKHostedAuthenticateViewController will be dismissed, since there no publick API to access that.
gameCenterLoginVCDismissObservation = gcLoginVC.observe(\.isBeingDismissed) { [weak self] controller, change in
self?.gameCenterLoginVCDismissObservation?.invalidate()
print(change)
}
source to share