13

Is there a notification that I can listen to that will tell me when an iPhone's volume is turned up?

I know about the AVSystemController_SystemVolumeDidChangeNotification, but it is essential that the notification only be triggered when the volume has been turned up, not up or down.

Secondly, how can I hide the translucent view that appears when the volume up button is pressed, showing the system's volume? Camera+ has implemented this.

User97693321
  • 3,336
  • 7
  • 45
  • 69
The Kraken
  • 3,158
  • 5
  • 30
  • 67

4 Answers4

37

If you want an event you can register a listener on the "outputVolume" property:

- (void)viewWillAppear:(BOOL)animated {

    AVAudioSession* audioSession = [AVAudioSession sharedInstance];

    [audioSession setActive:YES error:nil];
    [audioSession addObserver:self
                    forKeyPath:@"outputVolume"
                       options:0
                       context:nil];
}

-(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    if ([keyPath isEqual:@"outputVolume"]) {
        NSLog(@"volume changed!");
    }
}
Stephen
  • 371
  • 1
  • 3
  • 2
  • 5
    I have looked at so many places over stackoverflow and this is the only one using non-deprecated methods and strings to get this done. – ShayanK Apr 24 '14 at 21:18
  • 14
    The problem with this solution is that if the outputVolume is already max and they press the + button, the callback doesn't happen. – Ray Fix Jan 19 '15 at 22:48
  • this solution is really nice, but ray is right :( is it possible to reset the value programatically to 0? – donmarkusi Jan 22 '15 at 10:58
  • How would I be able to have the action be called also when the volume is max? – AustinT Feb 17 '15 at 23:41
  • @AustinT @donmarkusi You could try reading the current volume, then setting it back to (max - 1) if it's at the max. Look at `UISlider` and `setValue:animated:` to set it for `UIControlEventTouchUpInside`. – JaredH Feb 19 '15 at 19:35
  • Hey @JaredH, would it be possible to change the volume to a preset value if the user changes the volume up or down? I want that the volume stay in the same place no matter what the user does. – the_moon Mar 02 '15 at 19:05
  • 1
    this is not fired when volume is full and press high volume key and same when no volume and press low volume key. is there any solution? – Kumar Sep 10 '15 at 11:25
  • this line is crucial: `[audioSession setActive:YES error:nil]`. without it you can't observe change. – wzso Jul 21 '18 at 03:36
  • is it worked in iOS simulator ? is not working for me in simulator. – Hari Narayanan Jul 12 '19 at 11:49
  • Don't forget to remove the observer with `[audioSession removeObserver:self forKeyPath:@"outputVolume"]` – Roi Danton Aug 06 '19 at 15:38
17

There is no documented way to to this, but you can use this workaround. Register for AVSystemController_SystemVolumeDidChangeNotification notification and add an MPVolumeView which will prevent the system volume view from showing up.

MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:CGRectMake(-100, 0, 10, 0)];
[volumeView sizeToFit];
[self.view addSubview:volumeView];

And don't forget to start an Audio Session

AudioSessionInitialize(NULL, NULL, NULL, NULL);
AudioSessionSetActive(true);

In This case, the MPVolumeView is hidden from the user.

As for checking if volume up or down was pressed, just grab the current application's volume

float volumeLevel = [[MPMusicPlayerController applicationMusicPlayer] volume];  

and compare it with new volume after the button was pressed in notification callback

If you don't want to do it by yourself, there's a drop-in class available in github

https://github.com/blladnar/RBVolumeButtons

srgtuszy
  • 1,548
  • 1
  • 18
  • 16
  • One thing I've noticed about using this method is that if I have another app playing music in the background (or potentially if you were already playing music in the app where you were doing this detection), that I would stop receiving notifications and the volume buttons would go back to just changing the volume of the music playing in the background =/ – Will Aug 20 '13 at 12:51
  • `[[MPMusicPlayerController applicationMusicPlayer] volume]` has been deprecated since iOS 7. – JaredH Feb 19 '15 at 23:09
3

I solved this problem by adding own target/action for UISlider placed inside MPVolumeView. So it's possible to catch volume change events and determine what button was pressed. Here's github repo with implementation of this approach. It works fine with iOS 7 and above, no deprecation warnings and no rejection from Apple.

vitalytimofeev
  • 261
  • 2
  • 9
1

In order to distinguish volume action: INSTEAD OF (in observeValue guard)

temp != 0.5

USE for only volume up

temp > 0.5

and only detect volume down:

temp < 0.5 

This solution below will print if either volume up or down are pressed.

import AVFoundation
import MediaPlayer

override func viewDidLoad() {
  super.viewDidLoad()
  let volumeView = MPVolumeView(frame: CGRect.zero)
  for subview in volumeView.subviews {
    if let button = subview as? UIButton {
      button.setImage(nil, for: .normal)
      button.isEnabled = false
      button.sizeToFit()
    }
  }
  UIApplication.shared.windows.first?.addSubview(volumeView)
  UIApplication.shared.windows.first?.sendSubview(toBack: volumeView)
}

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
  do { try AVAudioSession.sharedInstance().setActive(true) }
  catch { debugPrint("\(error)") }   
}

override func viewDidDisappear(_ animated: Bool) {
  super.viewDidDisappear(animated)
  AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
  do { try AVAudioSession.sharedInstance().setActive(false) } 
  catch { debugPrint("\(error)") }
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  guard let key = keyPath else { return }
  switch key {
    case "outputVolume":
      guard let dict = change, let temp = dict[NSKeyValueChangeKey.newKey] as? Float, temp != 0.5 else { return }
      let systemSlider = MPVolumeView().subviews.first { (aView) -> Bool in
        return NSStringFromClass(aView.classForCoder) == "MPVolumeSlider" ? true : false
      } as? UISlider
      systemSlider?.setValue(0.5, animated: false)
      guard systemSlider != nil else { return }
      debugPrint("Either volume button tapped.")
    default:
      break
  } 
}
jnblanchard
  • 1,182
  • 12
  • 12