23

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?

Eric Aya
  • 69,000
  • 34
  • 174
  • 243
AustinT
  • 1,868
  • 7
  • 38
  • 62

5 Answers5

30

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
    }
}
Arsen Khachaturyan
  • 7,335
  • 4
  • 37
  • 38
rakeshbs
  • 23,816
  • 6
  • 72
  • 63
20

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")
  }
}
Matthijs
  • 475
  • 4
  • 8
  • 5
    This is not working for me - at least not in the simulator. – Ken Roy Apr 01 '18 at 14:24
  • 1
    Not work in simulator and also not trigger when already the max volume. – Nike Kov Sep 07 '18 at 07:50
  • 1
    Thanks - confirmed working in iOS 12 / xcode 10 / iPhone X, though you need to add this line below (I have it as a class variable on the View Controller): var audioSession = AVAudioSession() – woody121 Oct 02 '18 at 19:51
  • confirmed working in iOS 12 / xcode 10.2.1 / iPhone SE,no need to have audiosession as class property. – songgeb Jun 19 '19 at 08:01
8

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)")
}
Matheus Lima
  • 139
  • 2
  • 7
  • On iOS 14 it works without having to create `MPVolumeView`, unsure if this is the case with an earlier OS though – Ben Sullivan Dec 08 '20 at 11:52
  • @BenSullivan on my iOS 14.6 iPad it still needs to create `MPVolumeView`... – Saafo Jul 29 '21 at 08:29
  • Can anyone confirm if this breaks on iOS 15? The volumeChanged selector isn't getting called for me. – Nate Oct 28 '21 at 09:52
  • Tested it on iOS 15 and it seems to NOT be working. Does anyone know of a different method to detect the reason for volume changes? – Yoni Reiss Nov 29 '21 at 19:17
4
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.

jnblanchard
  • 1,096
  • 10
  • 12
1

If interested here is a RxSwift version.

func volumeRx() -> Observable<Void> {
    Observable<Void>.create {
        subscriber in
        
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(true)
        } catch let e {
            subscriber.onError(e)
        }

        let outputVolumeObserve = audioSession.observe(\.outputVolume) {
            (audioSession, changes) in
            subscriber.onNext(Void())
        }
        
        return Disposables.create {
            outputVolumeObserve.invalidate()
        }
    }
}

Usage

volumeRx()
   .subscribe(onNext: {
      print("Volume changed")
   }).disposed(by: disposeBag)
Dario Pellegrini
  • 1,486
  • 11
  • 13