32

What's the best/cleanest way of capturing volume up/down button presses on iOS 8?

Ideally I'd like to capture the keypress and also prevent the system volume from changing (or at the very least, prevent the volume change HUD from showing).


There are some old answers going around which use deprecated methods and don't seem to work at all on iOS 8. This iOS 8 specific one didn't work either.

This RBVolumeButtons open source class doesn't seem to work on iOS 8 either.

Community
  • 1
  • 1
Ricardo Sanchez-Saez
  • 9,466
  • 8
  • 53
  • 92
  • Is this for a jailbroken or enterprise app? If not, there is no way Apple would pass review on this. – Gary Riches Jan 28 '15 at 13:54
  • 1
    No, it's for a regular App Store one. I have read that Apple now approves capturing the volume buttons under some specific circumstances (e.g., using them as the camera shutter, see [Camera+](http://www.theverge.com/apple/2011/11/17/2569563/camera-app-iphone-updated-volume-button-shutter)). – Ricardo Sanchez-Saez Jan 28 '15 at 14:01
  • 1
    See my answer to the duplicate question here: http://stackoverflow.com/a/37360733/893774 I believe JPSVolumeButtonHandler is the cleanest way right now, in particular since the recent 1.0.1 fix. – marco May 23 '16 at 10:08
  • I agree with @marco, [JPSVolumeButtonHandler](https://github.com/jpsim/JPSVolumeButtonHandler) works great on iOS 8.x and iOS 9.x. I had an answer stating that but got deleted without warning. ¯\_(ツ)_/¯ – Ricardo Sanchez-Saez Jun 16 '16 at 11:57

9 Answers9

15

For Swift you can use the code below in your viewController class:

let volumeView = MPVolumeView(frame: CGRectMake(-CGFloat.max, 0.0, 0.0, 0.0))
self.view.addSubview(volumeView)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(volumeChanged(_:)), name: "AVSystemController_SystemVolumeDidChangeNotification", object: nil)

Then add this function

func volumeChanged(notification: NSNotification) {

     if let userInfo = notification.userInfo {
        if let volumeChangeType = userInfo["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String {
            if volumeChangeType == "ExplicitVolumeChange" {
                // your code goes here
            }
        }
    }
}

This code detect the explicit volume change action by the user, as if you didn't check of the explicit action, this function will be automatically called periodically.

This code doesn't prevent the system volume change.

Meow
  • 134
  • 1
  • 11
Mohammed Elrashidy
  • 1,820
  • 1
  • 14
  • 16
  • 3
    would you know how to prevent system to display volume hud? – Yuliani Noriega Jan 19 '17 at 06:00
  • Getting weird behaviour with this one (iOS 12 only tested so far) where this triggers when the lock button is pressed too. Also, to "prevent" the display of the volume view, create your own `MPVolumeView`, add it as a subview, and set its origin to be outside of the visible view. – shim Nov 08 '18 at 16:31
  • Is there a way to detect long-press of volume button rockers ? – Ali Raza Mar 04 '21 at 13:53
13

First add AVFoundation and MediaPlayer Framework and then you can use below code to detect up/down button press,

-(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"]) {        
      float volumeLevel = [[MPMusicPlayerController applicationMusicPlayer] volume];
      NSLog(@"volume changed! %f",volumeLevel);
  }
}
MFP
  • 1,141
  • 9
  • 22
  • 10
    Nice solution, but keep in mind that it won't detect the 'volume up' button if the volume is already at max (or the 'down' if its already on mute – Eliktz Aug 22 '16 at 18:03
  • 2
    just wondering, wont this go against apple's terms and conditions, as mentioned here http://stackoverflow.com/questions/29923664/is-it-possible-to-disable-volume-buttons-in-ios-apps ? – daisura99 Dec 16 '16 at 17:24
  • Even I would like to know the same and is it possible to programmatically reverse the volume change? (so that volume level remains at the original level – Shravya Boggarapu Jan 09 '17 at 09:11
  • @Eliktz is there any solution for this case?) – Roman Simenok Jul 20 '18 at 15:42
  • 1
    @daisura99 this guideline has been removed now and this behaviour is allowed, at least for triggering taking photos – lewis Jun 10 '21 at 14:02
11

for swift 3: (remember to add: import MediaPlayer.. )

    override func viewDidLoad() {
        super.viewDidLoad()

        let volumeView = MPVolumeView(frame: CGRect(x: 0, y: 40, width: 300, height: 30))
        self.view.addSubview(volumeView)
//      volumeView.backgroundColor = UIColor.red
        NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged(notification:)),
                                               name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"),
                                               object: nil)
    }


    func volumeChanged(notification: NSNotification) {

        if let userInfo = notification.userInfo {
            if let volumeChangeType = userInfo["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String {
                if volumeChangeType == "ExplicitVolumeChange" {
                    // your code goes here
                }
            }
        }
    }

....

ingconti
  • 10,876
  • 3
  • 61
  • 48
6

This is a 2 part answers, and they are independent, in Swift 5.

To listen to the volume trigger event,

import MediaPlayer

// Observe in eg. viewDidLoad
let volumeChangedSystemName = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")
NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged), name: volumeChangedSystemName, object: nil)

@objc private func volumeChanged(notification: NSNotification) {
    guard
        let info = notification.userInfo, 
        let reason = info["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String,
        reason == "ExplicitVolumeChange" else { return }

    // Handle it
}

To hide the system volume control,

// Add the view but not visible
let volumeView = MPVolumeView(frame: CGRect(x: -CGFloat.greatestFiniteMagnitude, y: 0, width: 0, height: 0))
view.addSubview(volumeView)
samwize
  • 25,675
  • 15
  • 141
  • 186
  • This solution is working even if max / min volume reached and hides system volume popup. – Volodymyr Kulyk Jul 30 '20 at 15:18
  • 1
    If I press and hold a volume button, the volume changes by one step, then there's a delay of about 300ms, then the volume changes really fast. After I release the volume button, the volume stops changing. This is enough to get a "button pressed" and "button released" type of event but I'm wondering if there's some way to shorten that delay between the first step and the fast changes. That would mean I don't have to wait such a long time to know if the button has been released. I'm guessing it's impossible but I just thought I'd ask. ;-) – Indiana Kernick Jan 28 '21 at 06:55
3

Objective-C version (using notifications):

#import <MediaPlayer/MPVolumeView.h>
#import <AVFoundation/AVFoundation.h>

@interface ViewController () {
    UISlider *volumeViewSlider;
}

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self startTrackingVolumeChanges];
}

- (void)dealloc
{
    [self stopTrackingVolumeChanges];
}

#pragma mark - Start Tracking Volume Changes

- (void)startTrackingVolumeChanges
{
    [self setupVolumeViewSlider];
    [self addObserver];
    [self activateAudioSession];
}

- (void)setupVolumeViewSlider
{
    MPVolumeView *volumeView = [[MPVolumeView alloc] init];
    for (UIView *view in [volumeView subviews]) {
        if ([view.class.description isEqualToString:@"MPVolumeSlider"]) {
            volumeViewSlider = (UISlider *)view;
            break;
        }
    }
}

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

- (void)activateAudioSession
{
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    [audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
    NSError *error;
    BOOL success = [audioSession setActive:YES error:&error];
    if (!success) {
        NSLog(@"Error activating audiosession: %@", error);
    }
}

#pragma mark - Observing Volume Changes

- (void)volumeChanged:(NSNotification *)notification
{
    NSString *volumeChangeType = notification.userInfo[@"AVSystemController_AudioVolumeChangeReasonNotificationParameter"];
    if ([volumeChangeType isEqualToString:@"ExplicitVolumeChange"]) {
        float volume = volumeViewSlider.value;
        NSLog(@"volume = %f", volume);
    }
}

#pragma mark - Stop Tracking Volume Changes

- (void)stopTrackingVolumeChanges
{
    [self removeObserver];
    [self deactivateAudioSession];
    volumeViewSlider = nil;
}

- (void)removeObserver
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];
}

- (void)deactivateAudioSession
{
    dispatch_async(dispatch_get_main_queue(), ^{
        NSError *error;
        BOOL success = [[AVAudioSession sharedInstance] setActive:NO error:&error];
        if (!success) {
            NSLog(@"Error deactivating audiosession: %@", error);
        }
    });
}

@end
Terry
  • 339
  • 4
  • 5
3

Combine Solution (Tested with Swift 5.2 / iOS 14)

  • Ensure you import the necessary frameworks into your file;
import Combine
import MediaPlayer
  • Set a variable for your Combine AnyCancellable, outside of a function;

var volumeCancellable: AnyCancellable?

  • In whatever function is relevant for your needs (viewDidLoad or elsewhere), configure the Combine subscriber;
volumePublisher = NotificationCenter.default
   .publisher(for: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"))
   .compactMap { $0.userInfo!["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String }
   .filter { $0 == "ExplicitVolumeChange" }
   .sink(receiveValue: { (val) in
       // Do whatever you'd like here
   })
  • In whatever function is relevant for your needs (again, viewDidLoad, or elsewhere), set up the volume control and position off-screen;
let volumeView = MPVolumeView(frame: CGRect(x: -CGFloat.greatestFiniteMagnitude, y: 0, width: 0, height: 0))
view.addSubview(volumeView)

Based on @samwize's great response above.

ZbadhabitZ
  • 2,753
  • 1
  • 25
  • 45
  • This solution has much shorter delays between each time the event is fired. So you can detect when the button is released without a 0.5 second delay. Awesome! – Steven Love Apr 28 '21 at 17:42
3

Swift 5 / iOS 15

On iOS 15 Notification name is renamed to SystemVolumeDidChange.

// hide volume indicator
let volumeView = MPVolumeView(frame: CGRect(x: -CGFloat.greatestFiniteMagnitude, y: 0.0, width: 0.0, height: 0.0))
self.view.addSubview(volumeView)

Add notification observer

NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged), name: NSNotification.Name(rawValue: "SystemVolumeDidChange"), object: nil)

Listen for volume changes

private var notificationSequenceNumbers = Set<Int>()
@objc func volumeChanged(notification: Notification) {
    if let userInfo = notification.userInfo {
        if let volumeChangeType = userInfo["Reason"] as? String,
           volumeChangeType == "ExplicitVolumeChange", let sequenceNumber = userInfo["SequenceNumber"] as? Int {
            DispatchQueue.main.async {
                if !self.notificationSequenceNumbers.contains(sequenceNumber) {
                    self.notificationSequenceNumbers.insert(sequenceNumber)
                    // handle volume change
                }
            }
        }
    }
}

Points to be noted:

  1. Notification on iOS 15 will be called twice even if you press volume button once, but their SequenceNumber is equal so we can use Set to handle notification just once.
  2. Notification callback isn't on main thread so we need to use DispatchQueue so that Set is consistent.
1

Ah ok, see the Audio Session Services References for more information. You need to start an audio session with AudioSessionInitialize and then make it active with AudioSessionSetActive, listen for changes in the volume with AudioSessionAddPropertyListener and pass a callback that has type AudioSessionPropertyListener.

This web site has a good write up: http://fredandrandall.com/blog/2011/11/18/taking-control-of-the-volume-buttons-on-ios-like-camera/

Gary Riches
  • 2,847
  • 1
  • 22
  • 19
1

Swift 5 / iOS 13

You already need an MPVolumeView in order to make the system hide the volume-change HUD. The simplest and most reliable solution I've found is to observe changes to the MPVolumeView itself.

During setup, usually in viewDidLoad():

// Create offscreen MPVolumeView
let systemVolumeView = MPVolumeView(frame: CGRect(x: -CGFloat.greatestFiniteMagnitude, y: 0, width: 0, height: 0))
myContainerView.addSubview(systemVolumeView)
let systemVolumeSlider = systemVolumeView.subviews.first(where:{ $0 is UISlider }) as? UISlider

// Observe volume changes (including from hardware buttons):
systemVolumeSlider.addTarget(self, action: #selector(volumeDidChange), for: .valueChanged)

Respond to volume changes:

@objc func volumeDidChange() {
    // Handle volume change
}
Robin Stewart
  • 3,147
  • 20
  • 29