program access to iPhone volume buttons

After the recent rejections from Apple

Do not use this. Apple now uses some patch which would reject your app straightaway if it uses any of the private APIs - though should note here that quite some apps on the App Store use this already and are still there!

The only way to do this now is to have an AVAudioPlayer prepared to play but not playing ([player prepareToPlay]). This seems to take care of adjusting the app's volume according to the rocker buttons.

There's no other published way currently to handle this.

PLEASE READ THE ABOVE NOTE

Yes, Use the MPVolumeView

MPVolumeView *volume = [[[MPVolumeView alloc] initWithFrame:CGRectMake(18.0, 340.0, 284.0, 23.0)] autorelease];
  [[self view] addSubview:volume];

  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(volumeChanged:) 
                                        name:@"AVSystemController_SystemVolumeDidChangeNotification" 
                                        object:nil];    
  for (UIView *view in [volume subviews]){
    if ([[[view class] description] isEqualToString:@"MPVolumeSlider"]) {
      volumeViewSlider = view;  //volumeViewSlider is a UIView * object
    }
  }
  [volumeViewSlider _updateVolumeFromAVSystemController];

-(IBAction)volumeChanged:(id)sender{
  [volumeViewSlider _updateVolumeFromAVSystemController];
}

This will give you a slider (same as one used in ipod) whose value will change acc to volume of the phone

You will get a compile-time warning that view may not respond to _updateVolumeFromAVSystemControl, but just ignore it.


If you just want to get the notifications, I think it is like this:

Please correct me if I am wrong, but I don't believe this uses any internal API.

[[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(volumeChanged:) 
        name:@"AVSystemController_SystemVolumeDidChangeNotification" 
        object:nil];

Details of this event are here: http://www.cocoadev.com/index.pl?AVSystemController

The other replies here seem to be based on this hack: http://blog.stormyprods.com/2008/09/proper-usage-of-mpvolumeview-class.html which was a workaround for a now-fixed bug.

But I'm pretty sure if all you want to do is GET the notification, and not SET the system volume, you can just use the notification center like with any other event!!

Be advised: since Apple added the volume-up action to the camera, this notification is not posted while a UIImagePickerController is visible.


The easiest and most functionally complete way to do this that I have found in studying all the sources mentioned above and in other threads is: JPSVolumeButtonHandler (I am not involved other than being a user. But thanks a lot to the people responsible!)

EDIT: Release 1.0.2 came with some significant changes/enhancements. I'll leave my prior answer for 1.0.1 below the fold.

I put a sample wrapper class that you can either deploy as-is, or use to learn JPSVolumeButtonHandler's hopefully proper use in a separate Github repository real quick here.

Here's how the wrapper is meant to be used (I'll add this to the repository as soon as I get to it):

  1. The singleton class has two flags: isInUse and isOn. isInUse is meant to be set in some sort of general app settings and switches button support on and off in general. So, no matter any other values in the class, if this is false nothing will happen when the user presses a volume button and the implementation makes sure as much as possible to keep things clean and not affect system volume level unnecessarily. (Read the issue mentioned in the README for what can happen, when button support is switched on for the first time.) isOn is meant to be true exactly for the duration that the button is needed. You can switch it on and off without regard to the present value of isInUse.

  2. In whichever view you initialize the action that's supposed to happen when a volume button gets pressed, set the action like so:

    PhysicalButton.shared.action = { /* do something */ }

The action has type () -> Void. Until you initialize the action, nothing will break. Just nothing will happen. This defensive functionality was important to me as the view that uses volume button support would only be created after button support is set up.

For seeing things in action, you can download the app that I am using this in real quick for free. The settings manipulate "Physical button support" in general. The main Stopwatch view is the one to actually switch button handling on when entering the view, and off on leaving it. If you find the time, you'll also find an important note there in Settings > User Guide > Option: Physical Button Support:

In exceptional circumstance, the app may not get a chance to properly switch volume button handling off outside the Stopwatch view...

I'll add the full note to the Github README.md. Feel free to adapt and reuse it, if it's relevant in your case.

The circumstances aren't actually that exceptional and I haven't fully figured out what's wrong. When the user kills the app (or you just stop your app from within Xcode) while volume buttons are on, physical button support may not properly be removed from the OS. Thus, you can end up with two internal handler instances, only one of which you have control over. So, then every button tap results in two or even more calls to the action routine. My wrapper has some guardian code to prevent too rapid an invocation of the button. But that's only a partial solution. The fix need to go into the underlying handler, which I regrettably still have too little an understand of to try to fix things myself.


OLD, FOR 1.0.1:

In particular, my interest was in a Swift solution. The code is in Objective-C. To save someone some research, this is all I did using Cocoapods (for dummies like me):

  1. Add pod 'JPSVolumeButtonHandler' to the podfile
  2. Run pod install on the command line
  3. Add #import <JPSVolumeButtonHandler.h> to the bridging header file
  4. Set up callbacks for the volume up and down buttons like so:

    let volumeButtonHandler = JPSVolumeButtonHandler(
        upBlock: {
            log.debug("Volume up button pressed...")
            // Do something when the volume up button is pressed...
        }, downBlock: {
            log.debug("Volume down button pressed...")
            // Do something else for volume down...
        })
    

That's it. The rest is optional.


In my case, I wanted to enable overlaying physical button pushes with virtual on-screen buttons just for select views, while making sure to block as little of the normal button functions as possible (so that the user can run music in the background and adjust its volume in the rest of the app just fine). I ended up with a mostly singleton class as follows:

class OptionalButtonHandler {

  static var sharedInstance: OptionalButtonHandler?

  private var volumeButtonHandler: JPSVolumeButtonHandler? = nil
  private let action: () -> ()

  var enabled: Bool {
    set {
        if !enabled && newValue {
            // Switching from disabled to enabled...
            assert(volumeButtonHandler == nil, "No leftover volume button handlers")
            volumeButtonHandler = JPSVolumeButtonHandler(upBlock: {
                log.debug("Volume up button pressed...")
                self.action()
                }, downBlock: {
                    log.debug("Volume down button pressed...")
                    self.action()
            })
        } else if enabled && !newValue {
            log.debug("Disabling physical button...")
            // The other way around: Switching from enabled to disabled...
            volumeButtonHandler = nil
        }
    }
    get { return (volumeButtonHandler != nil) }
  }

  /// For one-time initialization of this otherwise singleton class.
  static func initSharedInstance(action: () -> ()) {
      sharedInstance = OptionalButtonHandler(action: action)
  }

  private init(action: () -> ()) {
      self.action = action
  }
}

There is just one common action for both up and down volume buttons here. The initSharedInstance() was necessary, because my action included references to a UI element (a view) that would only be set up at some user-dependent point after app launch.

One-time set up like so:

OptionalButtonHandler.initSharedInstance({
    // ...some UI action
})

Enable/disable selectively simply like so:

OptionalButtonHandler.sharedInstance!.enabled = true  // (false)

(Notice that my code logic makes sure that .enabled is never accessed before initSharedInstance().)

I am running Xcode 7.3 and iOS 9.3.2 on the (required!) test device.

Looking forward to learning how Apple feels about overloading their precious volume buttons. At least my app makes sure to be minimally invasive and the button use really makes sense. It's not a camera app, but comparable apps have used physical volume buttons before (less nicely even).


If you are willing to dip into the private API, I have a patch to Wolf3d that adds exactly the functionality you are looking for. It uses the private AVSystemController class and some hidden methods on UIApplication


Okay,

So I saw your solutions and don't exactly know whether Apple is going to reject or accept using AVSystemController_SystemVolumeDidChangeNotification. But I have a work around.

Use UISlider of MPVolumeView for registering for any changes in volume by the iPhone hardware like this

MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:CGRectZero];

for (UIView *view in [volumeView subviews]) {
    if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
        self.volume_slider = (UISlider*)view;
        break;
    }
}
[volumeView sizeToFit];
#THIS IS THE MAIN LINE. ADD YOUR CALLBACK TARGET HERE
[self.volume_slider addTarget:self action:@selector(volumeListener:) forControlEvents:UIControlEventValueChanged];
[self addSubview:volumeView];
[volumeView setAlpha:0.0f];

-(void)volumeListener:(NSNotification*)notification {
     #UPDATE YOUR UI ACCORDING OR DO WHATEVER YOU WANNA DO.
     #YOU CAN ALSO GET THE SOUND STEP VALUE HERE FROM NOTIFICATION.
}

Let me know if this helps anyone.