View Controller not deallocated after adding GameKit - ios

My view controller no longer gets deallocated after adding the following:
#property (strong, nonatomic) GKLocalPlayer *player;
(in viewDidLoad)
self.player = nil;
[self authenticatePlayer];
- (void)authenticatePlayer
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
__unsafe_unretained typeof(*localPlayer) *blockLocalPlayer = localPlayer;
localPlayer.authenticateHandler =
^(UIViewController *authenticateViewController, NSError *error)
{
if (authenticateViewController != nil)
{
[self presentViewController:authenticateViewController animated:YES
completion:nil];
}
else if (blockLocalPlayer.isAuthenticated)
{
self.player = blockLocalPlayer;
[self openGame];
}
else
{
// Disable Game Center
self.player = nil;
[self openGame];
}
};
}
- (void)setPlayer:(GKLocalPlayer *)player
{
_player = player;
NSString *playerName;
if (_player)
{
playerName = _player.alias;
}
else
{
playerName = #"Anonymous Player";
}
NSLog(#"%#", [NSString stringWithFormat:#"Welcome %#", playerName]);
}
The problem occurs whether or not the user connects to game center. There must be something in the code that is causing the view controller to remain in memory after it gets dismissed. If I comment these lines out:
self.player = nil;
[self authenticatePlayer];
the view controller will properly get deallocated when dismissed.
EDIT:
My hunch was correct. From Apple docs:
Game Kit maintains a strong reference to your completion handler even
after successfully authenticating a local player. If your game moves
into the background, Game Kit automatically authenticates the player
again whenever your game moves back to the foreground. Game Kit calls
your same completion handler each time it authenticates the local
player. Be mindful that in block programming, any Objective-C object
referenced inside a block is also strongly referenced by the block
until the block is released. Because Game Kit maintains a strong
reference to your completion handler until your game terminates, any
objects referenced from within your authentication handler are also
held indefinitely.
This is a problem for me though. I'm using Cocos2d and it has problems resetting its view unless the view controller is completely deallocated and created fresh.
Is there any way to get Game Kit to let go of my view controller?

Got sick of dealing with the issue and moved the GameKit authentication code to a different view controller, where it is not vital that it be deallocated.

Related

Logic Behind when a Function is Called in Objective-C

I am having a hard time as a beginner to Objective-C with learning how and when a function is being called, as I am not seeing it explicitly stated. Below is some code for logging into, and playing a song from the Spotify SDK that I found online.
#import "AppDelegate.h"
#interface AppDelegate ()
#property (nonatomic, strong) SPTAuth *auth;
#property (nonatomic, strong) SPTAudioStreamingController *player;
#property (nonatomic, strong) UIViewController *authViewController;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.auth = [SPTAuth defaultInstance];
self.player = [SPTAudioStreamingController sharedInstance];
// The client ID you got from the developer site
self.auth.clientID = #"5bd669abf2a14fb59839c2c0570843fe";
// The redirect URL as you entered it at the developer site
self.auth.redirectURL = [NSURL URLWithString:#"spotlightmusic://returnafterlogin"];
// Setting the `sessionUserDefaultsKey` enables SPTAuth to automatically store the session object for future use.
self.auth.sessionUserDefaultsKey = #"current session";
// Set the scopes you need the user to authorize. `SPTAuthStreamingScope` is required for playing audio.
self.auth.requestedScopes = #[SPTAuthStreamingScope];
// Become the streaming controller delegate
self.player.delegate = self;
// Start up the streaming controller.
NSError *audioStreamingInitError;
NSAssert([self.player startWithClientId:self.auth.clientID error:&audioStreamingInitError],
#"There was a problem starting the Spotify SDK: %#", audioStreamingInitError.description);
// Start authenticating when the app is finished launching
dispatch_async(dispatch_get_main_queue(), ^{
[self startAuthenticationFlow];
});
return YES;
}
- (void)startAuthenticationFlow
{
// Check if we could use the access token we already have
if ([self.auth.session isValid]) {
// Use it to log in
[self.player loginWithAccessToken:self.auth.session.accessToken];
} else {
// Get the URL to the Spotify authorization portal
NSURL *authURL = [self.auth spotifyWebAuthenticationURL];
// Present in a SafariViewController
self.authViewController = [[SFSafariViewController alloc] initWithURL:authURL];
[self.window.rootViewController presentViewController:self.authViewController animated:YES completion:nil];
}
}
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary *)options
{
// If the incoming url is what we expect we handle it
if ([self.auth canHandleURL:url]) {
// Close the authentication window
[self.authViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
self.authViewController = nil;
// Parse the incoming url to a session object
[self.auth handleAuthCallbackWithTriggeredAuthURL:url callback:^(NSError *error, SPTSession *session) {
if (session) {
// login to the player
[self.player loginWithAccessToken:self.auth.session.accessToken];
}
}];
return YES;
}
return NO;
}
- (void)audioStreamingDidLogin:(SPTAudioStreamingController *)audioStreaming
{
[self.player playSpotifyURI:#"spotify:track:3DWOTqMQGp5q75fnVsWwaN" startingWithIndex:0 startingWithPosition:0 callback:^(NSError *error) {
if (error != nil) {
NSLog(#"*** failed to play: %#", error);
return;
}
}];
}
#end
I am wondering how exactly these functions are being called sequentially, and specifically how the audioStreamingDidLogin one is being run.
Additionally I was wondering how it would look to call that function from the view controller with some sort of input coming from the UI.
Any help with this logic would be greatly appreciated! Thanks.
Your question is closely tied to the Spotify framework being used. It is not a question of when Objective-C is executing something - the language has a standard sequential execution model - but how the framework is doing callbacks, e.g. audioStreamingDidLogin, to your code and utilising threads/GCD to do concurrent execution.
First you should read the Spotify framework documentation.
You can also place a breakpoint at the start of each method and then run under the debugger. When a breakpoint is hit check which thread has stopped and the stack trace. That should give you a good idea of execution flow and the concurrent threads being used.
HTH
UIApplicationDelegate method application:didFinishLaunchingWithOptions: is called first, followed by application:openURL:options:.
That first app delegate method sets self as the delegate for an AudioStreamingController. This is how audioStreamingDidLogin gets called. You're telling the streaming controller, "Tell me (self) when interesting things happen". (See the SPTAudioStreamingControllerDelegate docs for what else it might tell you about).
You probably wouldn't (shouldn't) call this function directly, especially if there's a chance that you might call it before auth is complete. Doing so would likely result in an error on the call to playSpotifyURI. If you're certain that the user is authenticated, then you don't need to call it. Just call what it calls: playSpotifyURI.

Audio file plays over itself

I'm building an app with multiple view controllers and I have coded an audio file to play at the start up of the app. That works fine and when I click on the button to view a different screen the audio file still plays without skipping a beat just like it's supposed to but my problem arises when I click on the button to go back to the main screen. When I click to go back to the main screen the audio file plays over itself reminding me of the song Row Row Your Boat. The app is re-reading that code that tells itself to play the audio file thus playing it all over again. My problem is that I can't figure out how to make it not do that. I have coded the app to stop the audio when clicking on the start game button, which is what I want it to do but not until then. I just need help getting the app to not play the audio file over itself when going back to the main screen. The audio file is coded to play infinitely until the "start" button is clicked. If anyone can make since out of what I'm trying to say then please help me code this thing correctly. Thanks to anyone who can make it work right.
Here my code:
-(void)viewDidLoad
{
NSString *introMusic = [[NSBundle mainBundle]pathForResource:#"invadingForces" ofType:#"mp3"];
audioPlayer0 = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:introMusic] error:NULL];
audioPlayer0.delegate = self;
audioPlayer0.numberOfLoops = -1;
[audioPlayer0 play];
}
The problem is that you start a sound in a local variable when your view is loaded, start it playing on endless repeat, and then forget about it. Then you close the view controller, leaving the now-forgotten audio player playing. Next time you invoke the view controller, it's viewDidLoad method creates another audio player and starts that one playing too, and then forgets about that one. Every time you open a new copy of that view controller, you'll start yet another sound player, adding another voice to your round of "row, row, row your boat."
The naive solution is to put the code that starts the sound player in the app delegate. Set up the AVAudioPlayer as a property of you app delegate. Create a startPlaying method and a stopPlaying method. In your didFinishLaunchingWithOptions method, call startPlaying.
It's cleaner app design not to put app functionality in your app delegate, but instead create a singleton to manage sound play. (Search on "iOS singleton design pattern" to learn more.) Create an appDidLaunch method in the singleton, and call appDidLaunch from didFinishLaunchingWithOptions to start playing your sound. That way the app delegate doesn't need to have app specific logic in it, but simply calls appDidLaunch and goes on it's way.
EDIT:
If you want to call a method in the app delegate, and your app delegate is declared as:
#interface AppDelegate : UIResponder <UIApplicationDelegate>
Then you'd call it from another file like this:
First, import the app delegate's header:
#import "AppDelegate.h"
And the actual code to call your app delegate's stopPlaying method:
//Get a pointer to the application object.
UIApplication *theApp = [UIApplication sharedApplication];
//ask the application object for a pointer to the app delegate, and cast it
//to our custom "AppDelegate" class. If your app delegate uses a different
//class name, use that name here instead of "AppDelegate"
AppDelegate *theAppDelegate = (AppDelegate *)theApp.delegate;
[theAppDelegate stopPlaying];
Here's some example code to wrap an AVAudioPlayer in a singleton -
BWBackgroundMusic.h
#interface BWBackgroundMusic : NSObject
// singleton getter
+ (instancetype)sharedMusicPlayer;
/* public interface required to control the AVAudioPlayer instance is as follows -
start - plays from start - if playing stops and plays from start
stop - stops and returns play-head to start regardless of state
pause - stops and leaves play-head where it is - if already paused or stopped does nothing
continue - continues playing from where the play-head was left - if playing does nothing
replace audio track with new file - replaceBackgroundMusicWithFileOfName:
set background player to nil - destroyBackgroundMusic
NOTE:- change default track filename in .m #define */
// transport like methods
- (void)startBackgroundMusic;
- (void)stopBackgroundMusic;
- (void)pauseBackgroundMusic;
- (void)continueBackgroundMusic;
// audio source management
- (void)replaceBackgroundMusicWithFileOfName:(NSString*)audioFileName startPlaying:(BOOL)startPlaying;
- (void)destroyBackgroundMusic;
#end
BWBackgroundMusic.m
#import "BWBackgroundMusic.h"
#import <AVFoundation/AVFoundation.h> // must link to project first
#define DEFAULT_BACKGROUND_AUDIO_FILENAME #"invadingForces.mp3"
#interface BWBackgroundMusic ()
#property (strong, nonatomic) AVAudioPlayer *backgroundMusicPlayer;
#end
#implementation BWBackgroundMusic
#pragma mark Singleton getter
+ (instancetype)sharedMusicPlayer {
static BWBackgroundMusic *musicPlayer = nil;
static dispatch_once_t onceToken;
dispatch_once (&onceToken, ^{
musicPlayer = [[self alloc] init];
});
//NSLog(#"sample rate of file is %f",[musicPlayer currentSampleRate]);
return musicPlayer;
}
#pragma mark Initialiser
- (id)init {
//NSLog(#"sharedMusicPlayer from BWBackgroundMusic.h init called...");
if (self = [super init]) {
// self setup _backgroundMusicPlayer here...
// configure the audio player
NSURL *musicURL = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/%#", [[NSBundle mainBundle] resourcePath], DEFAULT_BACKGROUND_AUDIO_FILENAME]];
NSError *error;
if (_backgroundMusicPlayer == nil) {
_backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:musicURL error:&error];
}
if (_backgroundMusicPlayer == nil) {
NSLog(#"%#",[error description]);
} else {
[self makePlaybackInfinite];
[_backgroundMusicPlayer play];
}
}
return self;
}
#pragma mark Selfish methods
- (void)makePlaybackInfinite {
// access backing ivar directly because this is also called from init method
if (_backgroundMusicPlayer) {
_backgroundMusicPlayer.numberOfLoops = -1;
}
}
- (CGFloat)currentSampleRate {
NSDictionary *settingsDict = [self.backgroundMusicPlayer settings];
NSNumber *sampleRate = [settingsDict valueForKey:AVSampleRateKey];
return [sampleRate floatValue];
}
#pragma mark Transport like methods
- (void)startBackgroundMusic {
// plays from start - if playing stops and plays from start
if (self.backgroundMusicPlayer.isPlaying) {
[self.backgroundMusicPlayer stop];
self.backgroundMusicPlayer.currentTime = 0;
[self.backgroundMusicPlayer prepareToPlay];// this is not required as play calls this implicitly if not already prepared
[self.backgroundMusicPlayer play];
}
else {
self.backgroundMusicPlayer.currentTime = 0;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];
}
}
- (void)stopBackgroundMusic {
// stops and returns play-head to start regardless of state and prepares to play
if (self.backgroundMusicPlayer.isPlaying) {
[self.backgroundMusicPlayer stop];
self.backgroundMusicPlayer.currentTime = 0;
[self.backgroundMusicPlayer prepareToPlay];
}
else {
self.backgroundMusicPlayer.currentTime = 0;
[self.backgroundMusicPlayer prepareToPlay];
}
}
- (void)pauseBackgroundMusic {
// stops and leaves play-head where it is - if already paused or stopped does nothing
if (self.backgroundMusicPlayer.isPlaying) {
[self.backgroundMusicPlayer pause];
}
}
- (void)continueBackgroundMusic {
// continues playing from where the play-head was left - if playing does nothing
if (!self.backgroundMusicPlayer.isPlaying) {
[self.backgroundMusicPlayer play];
}
}
#pragma mark Content management
- (void)replaceBackgroundMusicWithFileOfName:(NSString*)audioFileName startPlaying:(BOOL)startPlaying {
// construct filepath
NSString *filePath = [NSString stringWithFormat:#"%#/%#", [[NSBundle mainBundle] resourcePath], audioFileName];
// make a url from the filepath
NSURL *fileUrl = [NSURL fileURLWithPath:filePath];
// construct player and prepare
NSError *error;
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:&error];
[self.backgroundMusicPlayer prepareToPlay];
[self makePlaybackInfinite];
// if startplaying then play
if (startPlaying) {
[self.backgroundMusicPlayer play];
}
}
- (void)destroyBackgroundMusic {
// stop playing if playing
[self stopBackgroundMusic];
// destroy by setting background player to nil
self.backgroundMusicPlayer = nil;
}
#end
To use simply call [BWBackgroundMusic sharedMusicPlayer]; This will instantiate the singleton if not already instantiated, start the player automatically, and will loop infinitely by default.
Furthermore you can control it from any class that imports BWBackgroundMusic.h
For example to pause the player use
[[BWBackgroundMusic sharedMusicPlayer] pauseBackgroundMusic];

How to make my CMMotionManager available through a singleton?

So I have been designing an iOS game (in Sprite Kit) for a while now and just recently added a CMMotionManager to my project so that my character would be controlled by the tilt of the screen. It took some fiddling but I got it to work, and here's how I've implemented it:
In my initWithSize method I have
self.motionManager = [[CMMotionManager alloc] init];
self.referenceAttitude = nil;
Then, I've written methods beginMotionSensing and switchToLiveSensing. The first is intended to get an idea of how the phone is being held and the second starts the game itself. Using SKActions, I call the first method, wait a second, and call the second. They look something like this:
-(void)beginMotionSensing{
CMDeviceMotion *deviceMotion = self.motionManager.deviceMotion;
CMAttitude *attitude = deviceMotion.attitude;
self.referenceAttitude = attitude;
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) {
[self setNull:motion];
}];
self.motionManager.deviceMotionUpdateInterval = .3;
}
and,
-(void)switchToLiveSensing{
gameIsLive = YES;
[self.motionManager stopDeviceMotionUpdates];
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) {
[self captureRoll:motion];
}];
self.motionManager.deviceMotionUpdateInterval = .02;
}
So I had all of this set up and it was working great, until I added the ability to play the game more than once. Before this, I would simply have to close out of the app and quit the game when I lost. Obviously this became annoying so I added a "game over" scene, with the ability to go back and play the game scene again. The game scene is presented again like this:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
SKTransition *transition = [SKTransition fadeWithColor:[UIColor colorWithRed:147.0/255.0 green:213.0/255.0 blue:216.0/255.0 alpha:1.0] duration:2.0f];
MyScene *gameScene = [MyScene alloc];
gameScene = [gameScene initWithSize:self.frame.size passedInFuel:100];
[self.view presentScene:gameScene transition:transition];
}
I had remembered someone saying something in a previous post about a singleton - the comment was directed at someone implementing a CMMotionManager. They had said something along the lines of, "you should probably make it accessible through a singleton if you are going to initialize your class more than once." Well, sure enough, that's what I'm doing now. I had forgotten about this until I was playing the game and noticed that suddenly the phone seemed to ignore my tilt and went along with it's own business, killing my character in the process. It hasn't happened since, but is clearly something that needs to be fixed.
Could someone help me implement this so-called "singleton"?
I'm not convinced this is the problem you had, as the CMMotionManager object should be released anyway when your SKScene goes away. Can you duplicate the problem by starting the game again quickly after it ends?
You could probably do it by adding a class method in your main view/navigation controller (which ever one stays around longest) to create a singleton and then make it available to your SKScene object.
e.g.
+ (CMMotionManager *)sharedInstance
{
static CMMotionManager *sharedCMMM = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedCMMM = [[CMMotionManager alloc] init];
// Do any other initialisation stuff here
});
return sharedCMMM;
}

GameCenter showsCompletionBanner keeps showing

In my game (I'm using SpriteKit, and therefore only support iOS 7), when a player reaches his first 10 points, he is awarded with an achievement. I've implemented the achievement method as follows:
-(void) First10Points
{
GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier: #"Achievement_First10Points"];
if (achievement)
{
achievement.showsCompletionBanner = YES;
achievement.percentComplete = 100.0;
NSArray *achievements = [NSArray arrayWithObjects:achievement, nil];
[GKAchievement reportAchievements:achievements withCompletionHandler:^(NSError *error) {
if (error != nil) {
NSLog(#"Error in reporting achievements: %#", error);
}
}];
}
}
This works fine and the achievement is indeed earned at 10 points, with the game center banner indicating this to the player during the game. However, when the banner disappears it reappears after a second or so and continues to do so until i terminate the game. The game can still be played while it does this loop thing. I can't seem to understand why it does this and I have not come across this problem while searching the web.
Anyone an idea? Or should I implement my achievements in another way?
One possibility is that you're calling the First10Points method multiple times. You should check if the player has already reached the 10 points achievement before presenting the achievement again. If they have indeed already reached it, then don't call the method.
Try adding a variable like BOOL first10 = NO; When you run your check (score == 10), set first10 = YES; Everytime before you call First10Points, ensure that (first10 == NO)

How to authenticate a LocalPlayer for Gamecenter

I am trying to authenticate a LocalPLayer but I get errors and I can't figure out how to fix them. I am getting this off the Apple Developers forums and it has errors.
"no visible #interface for ViewController2 declares the selector disable gamcenter"
No visible #interface for 'ViewController2' declares the selector
'showAuthenticationDialogWhenReasonable:'
No visible #interface for 'ViewController2' declares the selector 'authenticatedPlayer:'
- (void) authenticateLocalPlayer
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
localPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error){
if (viewController != nil)
{
[self showAuthenticationDialogWhenReasonable: viewController];
}
else if (localPlayer.isAuthenticated)
{
[self authenticatedPlayer: localPlayer];
}
else
{
[self disableGameCenter];
}
}];
}
The code you've written in your question was lifted directly from Apple's documentation (specifically the "Authenticating A Local Player" section).
As the comment in that code says:
showAuthenticationDialogWhenReasonable: is an example method name.
Create your own method that displays an authentication view when
appropriate for your app.
and
authenticatedPlayer: is an example method name. Create your own method
that is called after the loacal player is authenticated.
and
"disableGameCenter" is the same.
All of them refer to "self", which means your code is looking for those implementations within the ViewController2 object.

Resources