Reputation: 13880
I have a pageViewController (UIPageViewController) which has a child view controller (named soundPlayer).
sound player has static AVAudioPLayer *player
when navigating between pages we must have on player (singleton pattern).
so if I navigate between pages without playing sound, and finally back from these view controller (navigation controller) soundPlayer will be released.
but when I play sound and navigate between pages, with every page flipping a new soundPlayer doesn't release and a new one will create!
before playing sound with every page flip, only we have on soundPlayerViewController, but when we play sound with every page flip, soundPlayerViewController doesn't release and living objects increases.
before playing sound
after playing sound
ignore those leaks in my real app I don't have them
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
@class AppScrollView;
@interface SoundPlayerViewController : UIViewController <AVAudioPlayerDelegate>{
BOOL paused;
BOOL inBackground;
NSTimer *updateTimer;
UIImage *playBtnBG;
UIImage *pauseBtnBG;
UIImage *bookmarkedBtnBG;
}
@property (weak, nonatomic) IBOutlet UILabel *duration;
@property (weak, nonatomic) IBOutlet UILabel *currentTime;
@property (weak, nonatomic) IBOutlet UISlider *progressBar;
@property (nonatomic, strong) NSTimer *updateTimer;
@property (nonatomic, assign) BOOL inBackground;
@property (weak, nonatomic) IBOutlet UIButton *playButton;
- (IBAction)playButtonPressed:(UIButton *)sender;
- (IBAction)progressSliderMoved:(UISlider *)sender;
- (void)updateViewForPlayerState:(AVAudioPlayer *)player;
- (void)updateViewForPlayerStateInBackground:(AVAudioPlayer *)player;
- (void)updateViewForPlayerInfo:(AVAudioPlayer *)player;
- (void)updateCurrentTimeForPlayer:(AVAudioPlayer *)player;
- (void)registerForBackgroundNotifications;
- (void)updateBookmarkButton;
- (void)stopSoundPlayer;
@end
SoundPlayerViewController.m
#import "SoundPlayerViewController.h"
@interface SoundPlayerViewController ()
- (void)customizeAppearance;
@end
@implementation SoundPlayerViewController
static AVAudioPlayer *soundPlayer;
@synthesize numberFormatter;
@synthesize duration;
@synthesize currentTime;
@synthesize progressBar;
@synthesize updateTimer;
@synthesize inBackground;
@synthesize playButton;
#pragma mark - View lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self customizeAppearance];
paused = true;
[self registerForBackgroundNotifications];
NSError *error = nil;
NSString *fileName = @"sample";
NSString *fileType = @"m4a";
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType];
NSURL *soundURL = [NSURL fileURLWithPath:soundFilePath];
//soundPalyer is Static so there is one for all of instance objects of this class.
if (!soundPlayer) {
soundPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundURL error:&error];
if (soundPlayer) {
[self updateViewForPlayerState:soundPlayer];
[self updateViewForPlayerInfo:soundPlayer];
soundPlayer.numberOfLoops = 0;
soundPlayer.delegate = self;
updateTimer = nil;
}
}
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//if you pause soundPlayer and curl page you will notice soundPlayer won't update so
//I update sound player
[self updateViewForPlayerState:soundPlayer];
}
- (void)viewWillDisappear:(BOOL)animated
{
[self updateViewForPlayerState:soundPlayer];
[super viewWillDisappear:animated];
}
#pragma mark - my custom functions
- (void)customizeAppearance
{
//change slider appearacne
UIImage *minImage = [[UIImage imageNamed:@"sliderMinTrack"]
resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 0)];
UIImage *maxImage = [[UIImage imageNamed:@"sliderMaxTrack"]
resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 9)];
UIImage *thumbImage = [UIImage imageNamed:@"sliderThumb"];
[progressBar setMaximumTrackImage:maxImage
forState:UIControlStateNormal];
[progressBar setMinimumTrackImage:minImage
forState:UIControlStateNormal];
[[UISlider appearance] setThumbImage:thumbImage
forState:UIControlStateNormal];
[[UISlider appearance] setThumbImage:thumbImage
forState:UIControlStateHighlighted];
[playButton setImage:playBtnBG forState:UIControlStateNormal];
}
- (void)pausePlaybackForPlayer:(AVAudioPlayer *)player
{
[player pause];
[self updateViewForPlayerState:player];
}
- (void)startPlaybackForPlayer:(AVAudioPlayer *)player
{
//I must add paused = false here, else if I stop playing and bring front the app from background, sound player continue playing.
paused = false;
player.enableRate = YES;
[player prepareToPlay];
[player play];
[self updateViewForPlayerState:player];
}
- (IBAction)playButtonPressed:(UIButton *)sender
{
if (soundPlayer.playing == YES) {
[self pausePlaybackForPlayer:soundPlayer];
//I use this flag to control when the audio should be played
paused = true;
} else {
[self startPlaybackForPlayer:soundPlayer];
}
}
- (IBAction)progressSliderMoved:(UISlider *)sender
{
soundPlayer.currentTime = sender.value;
progressBar.maximumValue = soundPlayer.duration;
[self updateViewForPlayerState:soundPlayer];
}
- (void)stopSoundPlayer
{
if (self.mailPickerIsPresented) {
if ([soundPlayer isPlaying]) {
self.mailPickerhasPausedPlayback = true;
[self pausePlaybackForPlayer:soundPlayer];
}
}
else
{
[soundPlayer stop];
soundPlayer = nil;
}
}
- (void)updateViewForPlayerState:(AVAudioPlayer *)player
{
if (updateTimer)
[updateTimer invalidate];
if (player.playing) {
if(pauseBtnBG)
{
[playButton setImage:((player.playing == YES) ? pauseBtnBG : playBtnBG) forState:UIControlStateNormal];
}
updateTimer = [NSTimer scheduledTimerWithTimeInterval:.01 target:self selector:@selector(updateCurrentTime) userInfo:player repeats:YES];
} else {
[playButton setImage:((player.playing == YES) ? pauseBtnBG : playBtnBG) forState:UIControlStateNormal];
//I called this function below to set currentTimeLabel=0 after finishig playing
//[self updateCurrentTimeForPlayer:soundPlayer];
updateTimer = nil;
}
}
- (void)updateViewForPlayerStateInBackground:(AVAudioPlayer *)player
{
[self updateCurrentTimeForPlayer:player];
if (player.playing)
{
[playButton setImage:((player.playing == YES) ? pauseBtnBG : playBtnBG) forState:UIControlStateNormal];
}
else
{
[playButton setImage:((player.playing == YES) ? pauseBtnBG : playBtnBG) forState:UIControlStateNormal];
}
}
-(void)updateViewForPlayerInfo:(AVAudioPlayer*)player
{
duration.text = [NSString stringWithFormat:@"%d:%02d",(int)player.duration / 60, (int)player.duration % 60, nil];
progressBar.maximumValue = player.duration;
}
- (void)updateCurrentTime
{
[self updateCurrentTimeForPlayer:soundPlayer];
}
- (void)updateCurrentTimeForPlayer:(AVAudioPlayer *)player
{
currentTime.text = [NSString stringWithFormat:@"%d:%02d", (int)soundPlayer.currentTime / 60, (int)soundPlayer.currentTime % 60, nil];
progressBar.value = player.currentTime;}
#pragma mark background notifications
- (void)registerForBackgroundNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(setInBackgroundFlag)
name:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearInBackgroundFlag)
name:UIApplicationWillEnterForegroundNotification
object:nil];
}
- (void)setInBackgroundFlag
{
inBackground = true;
[self pausePlaybackForPlayer:soundPlayer];
}
- (void)clearInBackgroundFlag
{
inBackground = false;
//we are checking if palyer was playing, if so, we continue its playing.
if (paused == false) {
[self startPlaybackForPlayer:soundPlayer];
}
}
#pragma mark AVAudioPlayer delegate methods
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{if (flag == NO)
NSLog(@"Playback finished unsuccessfully");
[player setCurrentTime:0.0];
if (inBackground)
{
[self updateViewForPlayerStateInBackground:player];
}
else
{
[self updateViewForPlayerState:player];
}
}
// we will only get these notifications if playback was interrupted
- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)p
{
NSLog(@"Interruption begin. Updating UI for new state");
// the object has already been paused, we just need to update UI
if (inBackground)
{
[self updateViewForPlayerStateInBackground:p];
}
else
{
[self updateViewForPlayerState:p];
}
}
- (void)audioPlayerEndInterruption:(AVAudioPlayer *)p
{
NSLog(@"Interruption ended. Resuming playback");
[self startPlaybackForPlayer:p];
}
@end
I have uploaded a sample app
https://www.dropbox.com/s/9fxqsly62euhr8p/pageBasedApp.zip
Upvotes: 0
Views: 133
Reputation: 4380
I would recommend you to create Singleton to manage your AVAudioPlayer as follows
Interface
// AudioManager.h
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@interface AudioManager : NSObject
@property (nonatomic, strong) AVAudioPlayer *soundPlayer;
+ (id)sharedManager;
@end
// AudioManager.m
#import "AudioManager.h"
#define kSoundFileName @"sample.m4a"
Implementation
@implementation AudioManager
+ (id)sharedManager
{
static AudioManager *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
- (AVAudioPlayer *)soundPlayer
{
NSError *error;
if (_soundPlayer == nil)
{
_soundPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[self soundURL] error:&error];
}
return _soundPlayer;
}
- (NSURL *)soundURL
{
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:@"bgMusic" ofType:@"mp3"];
return [NSURL fileURLWithPath:soundFilePath];
}
@end
in other view controllers, you can refer to AVAudioPlayer property on singleton object where only single AudioManager object will be created as follows
#import "AudioManager.h"
[[[AudioManager sharedManager] soundPlayer] play];
Upvotes: 1
Reputation: 131481
Short answer: Don'e use static variables. You don't understand how they work, and are mis-using them. You should remove the "static" qualifier on your sound player.
Upvotes: 0