KVO lost when entering background, trying to fix - ios

I have this method, adding an observer in myViewController:
-(void) observeSpeakerVolume{
[[AVAudioSession sharedInstance] setActive:YES error:nil];
[[AVAudioSession sharedInstance] addObserver:self forKeyPath:#"outputVolume" options:NSKeyValueObservingOptionNew context:nil];
}
I invoke this method in the viewdidLoad of myViewController:
- (void)viewDidLoad {
[self performSelectorInBackground:#selector(observeSpeakerVolume) withObject:nil];
[super viewDidLoad];
}
and finally I monitor the observer as follows:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if ([keyPath isEqualToString:#"outputVolume"]) {
if ([[change valueForKey:#"new"] isEqualToNumber:#0]){
...
} else {
...
}
}
}
This all works fine, but as soon as my app is send to the background, eg by hitting the home button, the observer seems to be automatically removed.
To fix that, I do this in my AppDelegate:
- (void)applicationWillEnterForeground:(UIApplication *)application {
myViewController * wfc = [[myViewController alloc]init];
[wfc observeSpeakerVolume];
}
That seems to work, however, switching back and forth several times between back- and foreground, eventually changing the volume of my iPhone crashes the app.
I tried to manually remove the observer when entering the background, but that also crashes the app, supporting the suggestion that the OS removes the observer automatically when entering the background.
So, I assume, that I'm adding the observer several times, but I don't see where, since ViewDidLoad is not called when the app enters the foreground.
What is wrong in my logic here?
thanks for your insights.

Bit late, but I had similar issue.
If you call
[[AVAudioSession sharedInstance] setActive:YES error:nil];
after your app is back from background e.g. in
- (void)applicationWillEnterForeground:(UIApplication *)application;
then the observer will work again.

Related

dispatch_async crashes app when re-used

I'm using a solution for here to make titleView clipsToBounds always true.
I have this in my ViewController and it works well, however, if I leave the ViewController by pressing the back button and then come back, it app crashes at the dispatch_async line.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if([object isEqual:[[self.navigationController.navigationBar subviews] objectAtIndex:2]]) {
dispatch_async(dispatch_get_main_queue(), ^{
[[self.navigationController.navigationBar subviews] objectAtIndex:2].clipsToBounds = NO;
[self.navigationItem.titleView layoutIfNeeded];
});
}
}
Edit:
The only error I get is: Thread 1: EXC_BAD_ACCESS (code=1, address=0x102d8860)
The console doesn't provide any information other than (lldb)
You must removeObserver if you go out from viewController.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.navigationController.navigationBar.subviews[2]
removeObserver:self
forKeyPath:#"clipsToBounds"];
}
Check to your block
if (self.navigationController.navigationBar.subviews.count > 1){
…

how to get call back each time when rootviewcontroller of UIWindow changes

I am new to iOS development. I am stuck in a problem where my use case is to get callback each time when the rootviewcontroller of UIWindow changes. I know there is a rootviewcontroller.tansiondelegate delegate property in rootviewcontroller but i am unable to get callback after using this delegate.
you can use the KVO to observe the change of property. like:
[[UIApplication sharedApplication].keyWindow addObserver:self forKeyPath:#"rootViewController" options:NSKeyValueObservingOptionNew context:#"rootViewControllerChange"];
and when the rootViewController is changed, will call method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([(__bridge NSString *)context isEqualToString:#"rootViewControllerChange"] ) {
// code what you want to do...
}
}
NOTE:
Don't forget to remove this observer when the instance of [self class] dealloc.
[[UIApplication sharedApplication].keyWindow removeObserver:self forKeyPath:#"rootViewController" context:#"rootViewControllerChange"];
if it is not working:
There is a more stupid method:☔️
you can use NSNotificationCenter, when you present a controller, you can
[[NSNotificationCenter defaultCenter] postNotificationName:#"PushNotificationPresentedController" object:nil userInfo:nil];
and addObserver to receive this Notification where you want to callback:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didPresentedController) name:#"PushNotificationPresentedController" object:nil];
(if i have another better method, i will update my answer.)
Got it working. I used method swizzling for this. Very useful concept.
Please refer this for method swizzling technique.
Refer this for similar question.

AVPlayer stops playing when buffer is full and dosent resume

I have a collection view with an AVPlayer inside the cell and the AVPlayer starts playing the AVPlayerItem in a loop when
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
gets called. this works well but the problem is that after the AVPlayer is playing the item a few times the video is no longer shown but i can hear its sound.
I also add an observer for the value #"playbackBufferFull" for each item that is played like that:
[item addObserver:self forKeyPath:#"playbackBufferFull" options:NSKeyValueObservingOptionNew context:nil];
i noticed that when the video stops the observer method of the value #"playbackBufferFull" gets called, first of all i would like to know what causes the buffer the get full, the second and most important is how can i resume the AVPlayer when the video stops;
i tried calling [cell.videoPlayer play]; and to replace the item with a new one and it didnt work, the observer method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if ([object isKindOfClass:[AVPlayerItem class]] && [keyPath isEqualToString:#"playbackBufferFull"])
{
//this method is get called when the video stop showing but i can still hear it
//how can i resume the video?
}
}
my solution is :
first add observe for AVPlayerItemPlaybackStalledNotification in viewDidLoad or ...
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemPlaybackStalledNotification
object:self.avPlayer.currentItem];
-(void)playerItemDidReachEnd:(NSNotification*)noti
{
//thisisn't good way but i can't find the best way for detect best place for resume again.
NSLog(#"\n\n give Error while Streaminggggg");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.avPlayer play];
});
}
BUT
maybe you can find the best way to call play method again for resume!
please check do you get AVPlayerStatusReadyToPlay keypatch ?
if you get , you can call play method there.
please notify me about the result

Updating UITableView after download is finished/started

I'm dealing with a tricky situation in my app. This app shows a list of files in a UITableView, from there you can download (I use AFnetworking 2.0) a file and then you can see download progress in another UITableView (all the views are in a UITabBarController).
My problem is that I'm not really sure about how to reload the UITableVIew showing current downloads when a new one is added or when one is finished or cancelled. I tried using KVO but it didn't work observing the operation queue from AFnetworking.
EDIT:
This is the KVO code I tried
In my downloads UITableViewCell
- (void)viewDidLoad
{
[super viewDidLoad];
[[[[PodcastManager sharedManager] sessionManager] operationQueue] addObserver:self
forKeyPath:NSStringFromSelector(#selector(operations))
options:NSKeyValueObservingOptionNew
context:nil];
}
and then...
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == [[[PodcastManager sharedManager] sessionManager] operationQueue]) {
if ([keyPath isEqualToString:NSStringFromSelector(#selector(operations))]) {
NSLog(#"OperationCount: %lu", (unsigned long)[[[[[PodcastManager sharedManager] sessionManager] operationQueue] operations] count] );
}
}
}
But this "solution" didn't work here.
In the past, I faced a similar situation with another app and then I used blocks, but this time, I want to avoid that solution because there is some part of my current app's code I want to reuse in the future.
Anybody has faced a similar situation can bring some light?
Thank you.
Why not just run
[tableView reloadData];
after each download? Maybe I"m missing something. But that's what I'd do.

iOS AVAudioPlayer Volume Control

I've read quite a few posts on the subject but the answers are not 100% clear. I'm looking for clarity here.
My app plays a short AVAudioPlayer sound periodically. The problem is, I can only set the volume after the first sound is played.
After reading stackoverflow, everyone seems to suggest that I play a dummy (silent) AVAudioPlayer sound at the start of the app to "link" the device's volume buttons to the "app volume".
Said another way, when the app starts, it's the "Ringer" volume that is controlled by default and only after the first sound is played will the device's volume buttons finally control the "app volume" (AVAudioPlayer volume) (it's the image without any label). Unfortunately by the time this happens, the user doesn't hear the first sound and now sees the app as broken.
My question is, is this the answer? Do I simply play a short dummy sound once at the start of the app to "link" the device's volume buttons to the app?
You don't have to play a dummy sound. Using the AudioToolbox framework you can set the AudioSessionActive as follows:
AudioSessionInitialize (NULL, NULL, NULL, NULL);
UInt32 sessionCategory = kAudioSessionCategory_AmbientSound;
AudioSessionSetProperty (kAudioSessionProperty_AudioCategory, sizeof (sessionCategory), &sessionCategory);
AudioSessionSetActive (true);
This will allow the volume buttons to control the app volume.
See this question: Cannot Control Volume of AVAudioPlayer via Hardware Buttons when AudioSessionActive is NO for more information on this approach.
Hey for future answer searchers, Since AudioSessionInitialize and AudioSessionSetActive are deprecated in iOS7 the recommended way of handling hardware audio and getting call backs is by using an AVAudioSession object. Set the session as active for your app and KVO on the #"outputVolume" property of the session.
- (id)init
{
self = [super init];
if (self)
{
self.audioSession = [AVAudioSession sharedInstance];
[_audioSession setActive:YES error:nil];
[_audioSession addObserver:self forKeyPath:#"outputVolume" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:NULL];
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"outputVolume"])
{
[self setVolume:[change[#"new"] floatValue]];
}
}
- (void)dealloc
{
[_audioSession removeObserver:self forKeyPath:#"outputVolume"];
[_audioSession setActive:NO error:nil];
}

Resources