I'm writing a game app for iOS that optionally uses Game Center with leaderboards for storing highscores. What I'm trying to achieve is:
first time the app get's started present the Game Center login viewController
if user logs in successfully everthing's fine
if user cancels the login process don't bother him anymore (not even on next app start)
if user selects a function that requires Game Center give him a chance to login
The builtin authenticateHandler doesn't capture a secong login attempt (already reported here) so the login viewController won't come up a second time if it was cancelled once before. So I tried to go this way and save a reference to the login viewController and present it when appropriate. When my highscores button is pressed the login viewController comes up as supposed. When the user cancels the login the viewController is dismissed (but only the first time). That means if he presses the button a second time the login viewController will come up again but can't be dismissed anymore by pressing the cancel button on the upper left corner.
Have a look at my code:
#interface GameKitHelper () {
UIViewController __weak *presentingVC;
}
#property (nonatomic, strong) NSArray *leaderBoards;
#property (nonatomic, strong) completionBlock completion;
#property (nonatomic, strong) UIViewController *gcLoginVC;
#end
- (void) authenticateLocalPlayerForce: (BOOL) force completion: (void (^)(BOOL)) completion {
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
if (localPlayer.authenticated && completion) {
completion(localPlayer.authenticated);
return;
}
self.completion = completion;
if (self.gcLoginVC && !localPlayer.authenticated && [[NSUserDefaults standardUserDefaults] boolForKey: CUserDeactivatedGC] && force) {
[self presentViewController: self.gcLoginVC force: force];
return;
}
localPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error) {
[self setLastError: error];
if ([GKLocalPlayer localPlayer].authenticated) {
[[NSUserDefaults standardUserDefaults] setBool: NO forKey: CUserDeactivatedGC];
if (self.completion) {
self.completion([GKLocalPlayer localPlayer].authenticated);
}
return;
}
if (viewController) {
self.gcLoginVC = viewController;
[self presentViewController: viewController force: force];
return;
}
[[NSUserDefaults standardUserDefaults] setBool: YES forKey: CUserDeactivatedGC];
[[NSUserDefaults standardUserDefaults] synchronize];
};
}
//on app start don't force presenting login viewController
- (void) viewDidLoad {
[super viewDidLoad];
[[GameKitHelper sharedGameKitHelper] authenticateLocalPlayerForce: NO completion:^(BOOL authenticated) {
if (authenticated) {
[[GameKitHelper sharedGameKitHelper] loadLeaderBoards];
}
}];
}
//when highscores button is pressed do force presenting login viewController
- (IBAction) highscoresButtonTapped: (UIButton *) sender {
[[GameKitHelper sharedGameKitHelper] authenticateLocalPlayerForce: YES completion:^(BOOL authenticated) {
if (authenticated) {
SGGameLeaderboardVC *vc = [[SGGameLeaderboardVC alloc] init];
[self.navigationController pushViewController: vc animated: YES];
}
}];
}
Related
I have a singleton class, and I have a property declared in it:
#property (nonatomic, strong) NSString *currentTableName;
+ (SuperNoteManager*)sharedInstance;
.m file:
+ (SuperNoteManager*)sharedInstance
{
static SuperNoteManager *_sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_sharedInstance = [[SuperNoteManager alloc] init];
});
return _sharedInstance;
}
When I run my app for the first time, there is no data in the data base,so it shows the EmptyViewController.
#property (nonatomic, strong) SuperNoteManager *myManager;
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
_myManager=[SuperNoteManager sharedInstance];
}
-(void)changeRootView{
UIStoryboard *storyboard=[UIStoryboard storyboardWithName:#"Main" bundle:nil];
HomeViewController *hVC=[storyboard instantiateViewControllerWithIdentifier:#"HomeViewController"];
UINavigationController *mNavVC=[storyboard instantiateViewControllerWithIdentifier:#"MainNavigationController"];
mNavVC.viewControllers=#[hVC];
[[UIApplication sharedApplication].keyWindow setRootViewController:mNavVC];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
if ( [_myManager checkForDataInAllTables]) {
NSLog(#"All tables are empty");
}else{
//a note is saved, show home view controller
if (![_myManager isDatabaseEmpty]) {
[self changeRootView];
}
}
}
There is + button on NavigationBar on EmptyNotesViewController, and on tap '+',
NotesViewController is pushed from EmptyNotesViewController.
In the NotesViewController, after I write some notes, I save the notes in database:
NotesViewController:
#property (nonatomic,strong) SuperNoteManager *myManager;
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
_myManager.currentTableName=#"WorkTable";
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController) {
NSLog(#"going back");
[self insertTextintoDatabase]; //Text is inserted . I double checked
}
}
And then When I go back to my EmpytNotesViewController, I check for data, and if data is present, I change the rootViewController as it is not EmptyNotesView anymore.
So When I go back to my EmptyNotesViewController:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
if ( [_myManager checkForDataInAllTables]) {
NSLog(#"All tables are empty");
}else{
//a note is saved, show home view controller
//Put a breakpoint here
if (![_myManager isDatabaseEmpty]) {
[self changeRootView];
}
}
}
Here at the breakpoint _myManager.currentTableName is nil. why?
I set it in the NotesController, and it became nil when it come back to the EmptyNotesController.
I thought once a value is set in singleton, it will persist as long as the app is closed/killed.
Note: I have declared the property of my Singleton class as strong and also all the properties in the singleton are declared as strong.
It appears like you never get a reference to the SuperNoteManager singleton in NotesViewController, like you did in your EmptyNotesController.
Therefore the currentTableName property never gets set in the first place.
You want to insert:
_myManager = [SuperNoteManager sharedInstance];
in your -viewDidAppear: before you set the currentTableName property.
I'm attempting to show a GADInterstitial ad when a user of my app clicks a certain button to go to a certain ViewController. In the new ViewController's viewDidLoad method I check to see if the GADInterstitial ad is ready, and if it is I attempt to present it. The problem is, when I dismiss the ad I notice that the ViewController that presented the ad is no longer there and instead it has sent me back to the ViewController that launched the ViewController that presented the ad.
Here's my code:
- (void)viewDidLoad {
[super viewDidLoad];
BTRInterstitialHelper *helper = [BTRInterstitialHelper sharedHelper];
if([helper isInterstitialReady]) {
[helper.interstitial presentFromRootViewController:self];
}
My helper method just looks like this:
-(BOOL)isInterstitialReady {
if ([self.interstitial isReady]) {
return YES;
} else {
return NO;
}
}
I do notice thought that when I present the ad, this message shows up in the logs:
Presenting view controllers on detached view controllers is discouraged BTRIndividualLocationViewController: 0x7fd63bcd9c00.
Same problem occurs if I try to present it in viewWillAppear.
What am I doing wrong here?
EDIT:
I have also tried the following, to try to pass in my app's actual rootViewController:
UINavigationController *nav=(UINavigationController *)((BTRAppDelegate *)[[UIApplication sharedApplication] delegate]).window.rootViewController;
UIViewController *rootController=(UIViewController *)[nav.viewControllers objectAtIndex:0];
[helper.interstitial presentFromRootViewController:rootController];
Still the same result though, except that message doesn't pop up in the logs.
VC2.h
#import "GADInterstitial.h"
#import "GADInterstitialDelegate.h"
set delegate
#interface VC2 : UIViewController <GADInterstitialDelegate>
and a property
#property(nonatomic, strong) GADInterstitial *interstitial;
VC2.m
in viewdidappear
self.interstitial = [[GADInterstitial alloc] init];
self.interstitial.adUnitID = #"ca-app-pub-yourNumber";
self.interstitial.delegate = self;
GADRequest *request = [GADRequest request];
for test ads in simulator
request.testDevices = #[ GAD_SIMULATOR_ID ];
coud also add location and keywords in the request (optional)
then do request
[self.interstitial loadRequest:request];
listen to delegate
-(void)interstitialDidReceiveAd:(GADInterstitial *)ad {
if ([self.interstitial isReady]) {
[self.interstitial presentFromRootViewController:self];
}
}
If you want to show the interstitial only when pressed from a certain button use NSUserdefaults.
In VC1 when button pressed
[[NSUserDefaults standardUserDefaults] setValue:#"yes" forKey:#"showInterstitial"];
[[NSUserDefaults standardUserDefaults] synchronize];
and wrap all the code for the interstitial in VC2 in viewdidappear in:
if ([[[NSUserDefaults standardUserDefaults] valueForKey:#"showInterstitial"] isEqualToString:#"yes"]) {
}
and add to -(void)interstitialDidReceiveAd:(GADInterstitial *)ad {
[[NSUserDefaults standardUserDefaults] setValue:#"no" forKey:#"showInterstitial"];
[[NSUserDefaults standardUserDefaults] synchronize];
p.s. you could also use a Bool for NSUserdefaults of course, I do use strings caus I had some errors with Bools, but Bools would be more correct I guess
A very similar scenario for me in 2018.
My current ViewControllers look like this:
ViewController 1
-> ViewController 2 (via presentViewController:animated:completion)
-> GADOInterstitialViewController (presentFromRootViewController:)
When GADOInterstitialViewController is dismissed via rewardBasedVideoAdDidClose:, ViewController 2 is instantly dismissed also.
A simple solution to this issue that I have figured out. Simply override ViewController 2 function dismissViewControllerAnimated like this:
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
if(self.presentedViewController != nil){
[self.presentedViewController dismissViewControllerAnimated:flag completion:completion];
} else {
[super dismissViewControllerAnimated:flag completion:completion];
}
}
This code block checks if ViewController 2 has a presented view controller and dismisses it, if not then ViewController 2 gets dismissed.
This works perfectly in my case :)
My question is based on the following question, but my case is reversed.
Example for login screen modally based on storyboard
I would like to have a few tabs private for users only so required login, my storyboard is below:
Below is my code in custom tabBarController
#interface PNTabBarController ()
#end
#implementation PNTabBarController
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
{
NSUInteger indexOfTab = [tabBar.items indexOfObject:item];
NSLog(#"%lu", (unsigned long)indexOfTab);
if (indexOfTab == 2 || indexOfTab == 3) {
PFUser *currentUser = [PFUser currentUser];
NSLog(#"%#", currentUser.username);
if(currentUser == nil){
PNLoginViewController *obj = [[PNLoginViewController alloc]init];
[self presentViewController:obj animated:YES completion:NULL];
}
else{
NSLog(#"%#", currentUser.username);
}
}
}
But what happens is a dark screen when I click the 2 or 3 tab. What should be the correct way to achieve this?
EDIT: Check below for solution.
I am working on a login screen for my app and I have it working for the most part except for a few edge cases. Ive set things up so that I have a segue from my UITabBar in the story board that I trigger in the app delegate applicationDidBecomeActive: method. As I said it works fine on all but one edge case I've found so far.
My app uses some modal view controllers, some of which are UIActivityViewControllers if that makes a difference, to enter and edit some core data entities. If one of these modal view controllers is opened when the app goes to the background, it will always show up when the app is reopened and my login doesn't show. I get the following console msg
Warning: Attempt to present <UINavigationController: 0x1d51e320> on <MPTabBarViewController: 0x1d5b4810> which is already presenting <UIActivityViewController: 0x1e38fc40>
Here is my code
- (void) displayLogin{
NSLog(#"%s", __PRETTY_FUNCTION__);
UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
NSDate *lastDate = [[NSUserDefaults standardUserDefaults] objectForKey:MPLastCloseDate];
NSTimeInterval timeDiff = [[NSDate date] timeIntervalSinceDate:lastDate];
int seconds = timeDiff;
if ([[NSUserDefaults standardUserDefaults] integerForKey:MPPassCodeDelay] == MPScreenLockAlways || seconds >= 300) {
NSLog(#"Should see login");
[tabBarController performSegueWithIdentifier:#"loginScreen" sender:self];
}
}
I understand exactly what this msg is telling me, the tab bar is already presenting a modal controller so it can't present another one. So my question is this, Is there a better way to implement this so that the login will always show, even over top of the modal views?
Okay here is my current solution
as suggested by Bartu and requested to be shared by Shawn
I have a working singleton loginManager class that requires 1 call in app delegate and 1 call in any view controller that could be called to present as modal. I was unable to figure out how to do this as suggested with a ViewController category, but hey a few includes and method calls aren't so bad. I included it in App-Prefix.pch, so its available everywhere. It is written for ARC, so if you like managing your own memory you'll need to modify the singleton for that. The last caveat, at current you will need to roll your own viewController for the login screen. Just look for the commented section in the implementation with all the stars, and put your own view controller there. Mine is still in my app storyboard, its basically 4 digit pin that checks for a match in the keychain and dismisses itself for the correct pin. I may pull that out of my storyboard and nib it so it could be packaged with the loginManager and let it become my first gitHub project at some future date though.
You can configure it to display login for every time the app opens or after a delay with properties. The delay time is also a property set in seconds. It will also block out your apps UI for the few seconds it takes to get the login displayed with a splash using your apps Default.png. This is also configurable with a property.
I would love to get some feedback on this, and if anyone can tell me how to do a category so the extra call in viewControllers is not needed that would be great! Enjoy!
AppDelegate:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
[self.window makeKeyAndVisible];
// these calls are all optional
[[VHLoginManager loginManager] setShouldBlockUIWithSplashOnResume:NO];
[[VHLoginManager loginManager] setSecondsRequiredToPassBeforeLockDown:1000];
[[VHLoginManager loginManager] setScreenLockRequirment:VHLMScreenLockDelayed];
// this is the only required call to run with defaults - always login and block UI with splash while login loads
[[VHLoginManager loginManager] presentLogin];
}
Any viewController that may presented as modal at some point
- (void)viewDidLoad
{
[super viewDidLoad];
[[VHLoginManager loginManager] registerViewControllerIfModal:self];
}
The loginManager class
header:
// VHLoginManager.h
// Created by Victor Hudson on 5/31/13.
// Copyright (c) 2013 Victor Hudson. All rights reserved.
// Use if you like but be nice and leave my name
#import <Foundation/Foundation.h>
#define VHLMLastCloseDate #"VHLMLastCloseDate"
#define VHLMPassCodeDelay #"VHLMPassCodeDelay"
typedef enum {
VHLMScreenLockAlways = 0,
VHLMScreenLockDelayed = 1,
} VHLMScreenLockRequirement;
#interface VHLoginManager : NSObject
#property (nonatomic) BOOL shouldBlockUIWithSplashOnResume;
// defaults to yes so app contents arent visible before the login screen appears
#property (nonatomic) int secondsRequiredToPassBeforeLockDown;
// defaults to 5 minutes (300)
#pragma mark - Class Methods
+ (VHLoginManager *)loginManager;
// returns the singleton login manager
#pragma mark - Manager Methods
- (void) presentLogin;
// will determine if login should be presented an do so if needed
- (void) registerViewControllerIfModal:(UIViewController *)controller;
// any view controllers that are presented modally should call this with self as controller in viewDidLoad - the pupose of this manager is so login shows even over top of modals
- (void) setScreenLockRequirment:(VHLMScreenLockRequirement) requirement;
// deafaults to always if not adjusted
#end
implementation:
// VHLoginManager.m
// Created by Victor Hudson on 5/31/13.
// Copyright (c) 2013 Victor Hudson. All rights reserved.
// Use if you like but be nice and leave my name
#import "VHLoginManager.h"
static VHLoginManager *loginManager = nil;
#interface VHLoginManager ()
#property (nonatomic, strong) UIViewController *currentModalViewController;
#property (nonatomic) VHLMScreenLockRequirement screenLockrequirement;
#end
#implementation VHLoginManager
#pragma mark - Manager Methods
- (void) presentLogin
{
// NSLog(#"%s", __PRETTY_FUNCTION__);
if ([[NSUserDefaults standardUserDefaults] integerForKey:VHLMPassCodeDelay] == VHLMScreenLockAlways || [self timeSinceLastClose] >= self.secondsRequiredToPassBeforeLockDown) {
//NSLog(#"User should see login");
// determine who the presenting view controller should be
UIViewController *viewController;
if (self.currentModalViewController && self.currentModalViewController.presentingViewController != nil) {
// NSLog(#"We have a modal view controller on top");
viewController = self.currentModalViewController;
} else {
// NSLog(#"We have NO modal view controller on top");
// get the root view controller of the app
viewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
}
//********************************************************************************************************************************************************************************
// *** This is still tied into my app storyboard and should be made into a viewcontroller with nib to be portable with loginManager for now implement and present your own loginViewController
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
UINavigationController *navController = [storyboard instantiateViewControllerWithIdentifier:#"appLoginScreen"];
//********************************************************************************************************************************************************************************
// present the login to user
[viewController presentViewController:navController animated:NO completion:nil];
}
}
- (void) setScreenLockRequirment:(VHLMScreenLockRequirement) requirement
{
_screenLockrequirement = requirement;
[[NSUserDefaults standardUserDefaults] setInteger:self.screenLockrequirement forKey:VHLMPassCodeDelay];
}
- (void) registerViewControllerIfModal:(UIViewController *)controller
{
// NSLog(#"%s", __PRETTY_FUNCTION__);
if (controller.presentingViewController) {
NSLog(#"Registering a modalViewController");
self.currentModalViewController = controller;
}
}
#pragma mark - Private Methods
- (void) timeStampForBackground
{
// NSLog(#"%s", __PRETTY_FUNCTION__);
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:VHLMLastCloseDate];
[self setDisplaySplashForBackgroundResume];
}
- (int) timeSinceLastClose
{
return [[NSDate date] timeIntervalSinceDate:[[NSUserDefaults standardUserDefaults] objectForKey:VHLMLastCloseDate]];
}
#pragma mark Splash Screen management
- (void) setDisplaySplashForBackgroundResume
{
// NSLog(#"%s", __PRETTY_FUNCTION__);
if (self.shouldBlockUIWithSplashOnResume) {
// dismiss all keyboards and input views
UIView *topView = [[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];
[topView endEditing:YES];
// Don't show a splash screen if the application is in UIApplicationStateInactive (lock/power button press)
UIApplication *application = [UIApplication sharedApplication];
if (application.applicationState == UIApplicationStateBackground) {
UIImageView *splash = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Default"]];
splash.frame = application.keyWindow.bounds;
[application.keyWindow addSubview:splash];
}
}
}
- (void) removeSplashScreen
{
// NSLog(#"%s", __PRETTY_FUNCTION__);
if (self.shouldBlockUIWithSplashOnResume) { // we should have a splash image up if true
// so remove it
UIWindow *thewindow = [[UIApplication sharedApplication] keyWindow];
if ([[thewindow subviews] count] > 1) {
[NSThread sleepForTimeInterval:1.0];
[[[thewindow subviews] lastObject] removeFromSuperview];
}
}
}
#pragma mark - Class Management
//prevent additional instances
+ (id)allocWithZone:(NSZone *)zone
{
return [self loginManager];
}
+ (VHLoginManager *)loginManager
{
if (!loginManager) {
//Create The singleton
loginManager = [[super allocWithZone:NULL] init];
}
return loginManager;
}
- (id) init
{
// If we already have an instance of loginManager
if (loginManager) {
//Return The Old One
return loginManager;
}
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(timeStampForBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(removeSplashScreen)
name:UIApplicationDidBecomeActiveNotification
object:nil];
self.shouldBlockUIWithSplashOnResume = YES;
self.secondsRequiredToPassBeforeLockDown = 300;
if (![[NSUserDefaults standardUserDefaults] integerForKey:VHLMPassCodeDelay]) {
[self setScreenLockRequirment:VHLMScreenLockAlways];
}
}
return self;
}
#end
I had the same problem a short time ago, my solution for this problem is to have a reference to any modal view which is currently presented in your app delegate. So, you can know if your tab bar controller is already presenting a modal controller and if it's the case, you can present your login view over your current modal view.
What I did was to have a switch in my appDelegate. when the app started, if the user had not logged in, I created the login view and make it the window's rootViewController. When the user successfully logged in, I used an animation block to set that view's alpha to 0, then created a UITabBarController, populated it, made it the window's rootViewController (with an alpha of 0, then animated it's alpha to 1). Worked really well. Not sure how to do this with storyboards though.
EDIT: now getting familiar with storyboards. So what you would do is not use the Main.storyboard per se (remove it from info.plist), then add a LoginViewController as a view, and have your UITabbarController there too - but nothing is the initial view controller. You obviously have to name each view so you can create it in code, but asking the Storyboard to create such and such a view controller
So in App Delegate, if logged in, instantiate the tab bar controller and add it as the root view controller. If the user has not logged in, create the LoginView and add it as rootview controller. If the user does login, have some method on the LoginViewController so it can ask the delegate to switch to the tab bar controller.
I have a problem with the integration of game center in my app' which use iOS 6 SDK.
In fact I use the sample code from Apple, but it looks like incomplete :
I have tried this code :
-(void) authenticateLocalPlayer {
GKLocalPlayer* localPlayer =
[GKLocalPlayer localPlayer];
localPlayer.authenticateHandler =
^(UIViewController *loginVC,
NSError *error) {
[self setLastError:error];
if ([GKLocalPlayer localPlayer].authenticated)
{
// authentication successful
[self enableGameCenterForPlayer:[GKLocalPlayer localPlayer]];
}
else if (loginVC)
{
// player not logged in yet, present the vc
[self pauseGame];
[self presentLoginVC:loginVC];
}
else
{
// authentication failed, provide graceful fallback
[self disableGameCenter];
}
};
}
But the problem is that enableGameCenterForPlayer, pauseGame, presentLoginVC, disableGameCenter are not implemented methods, and it returns :
Instance method '-enableGameCenterForPlayer:' not found (return type defaults to 'id')
How can I fix this problem ?
Thanks
I use the method [self presentLoginVC:VC] to pass my UITabViewController or UINavigationController the viewController because the block below is not on the main thread.
localPlayer.authenticateHandler = ^(UIViewController *loginVC, NSError *error) {
When you are in a block you should be sure not to change UI elements because you really don't know when it will complete or where you will be in your app. There are probably many ways to do this, but this is my solution.
Below is my UITabBarController 'category' .m file (additions of methods for a class without subclassing) I create the method presentLoginVC and just have it call 'showGameCenterViewController' through my UITabBarController:
#import "UITabBarController+GameKitAdditions.h"
#implementation UITabBarController (GameKitAdditions)
-(void) showGameCenterViewController: (UIViewController *)VC {
[self presentViewController:VC animated:NO completion:nil];
}
-(void)dismissGameCenterViewController:(UIViewController *)VC {
[self dismissViewControllerAnimated:YES completion:nil];
}
#end
As to the other functions:
-(void) enableGameCenterForPlayer:(GKLocalPlayer *) localPlayer;
-(void) disableGameCenter;
-(void) pauseGame;
They could be as simple as just setting a BOOL called enableGameCenter to YES or NO. To get around errors you can just add these prototypes to your .h file and then write the functions just to output something to NSLog() or something.