Detect volume button press

Volume button notification function is not being called.

Code:

func listenVolumeButton(){
    // Option #1
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "volumeChanged:", name: "AVSystemController_SystemVolumeDidChangeNotification", object: nil)
    // Option #2
    var audioSession = AVAudioSession()
    audioSession.setActive(true, error: nil)
    audioSession.addObserver(self, forKeyPath: "volumeChanged", options: NSKeyValueObservingOptions.New, context: nil)
}

override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
    if keyPath == "volumeChanged"{
        print("got in here")
    }
}

func volumeChanged(notification: NSNotification){
   print("got in here")
}

listenVolumeButton() is being called in viewWillAppear

The code is not getting to the print statement "got in here", in either case.

I am trying two different ways to do it, neither way is working.

I have followed this: Detect iPhone Volume Button Up Press?


Using the second method, the value of the key path should be "outputVolume". That is the property we are observing. So change the code to,

var outputVolumeObserve: NSKeyValueObservation?
let audioSession = AVAudioSession.sharedInstance()

func listenVolumeButton() {
    do {
        try audioSession.setActive(true)
    } catch {}

    outputVolumeObserve = audioSession.observe(\.outputVolume) { (audioSession, changes) in
        /// TODOs
    }
}

The code above won't work in Swift 3, in that case, try this:

func listenVolumeButton() {
   do {
    try audioSession.setActive(true)
   } catch {
    print("some error")
   }
   audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  if keyPath == "outputVolume" {
    print("got in here")
  }
}

With this code you can listen whenever the user taps the volume hardware button.

class VolumeListener {
    static let kVolumeKey = "volume"

    static let shared = VolumeListener()

    private let kAudioVolumeChangeReasonNotificationParameter = "AVSystemController_AudioVolumeChangeReasonNotificationParameter"
    private let kAudioVolumeNotificationParameter = "AVSystemController_AudioVolumeNotificationParameter"
    private let kExplicitVolumeChange = "ExplicitVolumeChange"
    private let kSystemVolumeDidChangeNotificationName = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")

    private var hasSetup = false

    func start() {
        guard !self.hasSetup else {
            return
        }

        self.setup()
        self.hasSetup = true

    }

    private func setup() {
        guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
            return
        }

        let volumeView = MPVolumeView(frame: CGRect.zero)
        volumeView.clipsToBounds = true
        rootViewController.view.addSubview(volumeView)

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(self.volumeChanged),
            name: kSystemVolumeDidChangeNotificationName,
            object: nil
        )

        volumeView.removeFromSuperview()
    }

    @objc func volumeChanged(_ notification: NSNotification) {
        guard let userInfo = notification.userInfo,
            let volume = userInfo[kAudioVolumeNotificationParameter] as? Float,
            let changeReason = userInfo[kAudioVolumeChangeReasonNotificationParameter] as? String,
            changeReason == kExplicitVolumeChange
            else {
                return
        }

        NotificationCenter.default.post(name: "volumeListenerUserDidInteractWithVolume", object: nil,
                                        userInfo: [VolumeListener.kVolumeKey: volume])
    }
}

And to listen you just need to add the observer:

NotificationCenter.default.addObserver(self, selector: #selector(self.userInteractedWithVolume),
                                           name: "volumeListenerUserDidInteractWithVolume", object: nil)

You can access the volume value by checking the userInfo:

@objc private func userInteractedWithVolume(_ notification: Notification) {
    guard let volume = notification.userInfo?[VolumeListener.kVolumeKey] as? Float else {
        return
    }

    print("volume: \(volume)")
}

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
  } 
}

When observing a new value, I set the system volume back to 0.5. This will probably anger users using music simultaneously, therefore I do not recommend my own answer in production.