AustinT
AustinT

Reputation: 2026

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?

Upvotes: 26

Views: 26932

Answers (6)

Ryan Sam
Ryan Sam

Reputation: 2997

SwiftUI Approach

Spent a days worth of work doing this as I'm new to Swift but I've come up with a solution that:
A: can read out the volume
B: works even if volume is at max or min
C: probably is customizable to your liking

struct VolumeEventReader<Content: View>: UIViewControllerRepresentable {
    let builder: (Float) -> Content

    class Coordinator: NSObject {
        var parent: VolumeEventReader
        var lastVolumeNotificationSequenceNumber: Int?
        var currentVolume = AVAudioSession.sharedInstance().outputVolume

        init(_ parent: VolumeEventReader) {
            self.parent = parent
        }

        @objc func volumeChanged(_ notification: NSNotification) {
            DispatchQueue.main.async { [self] in
                volumeControlIOS15(notification)
            }
        }

        func manageVolume(volume: Float, minVolume: Float) {
            switch volume {
            case minVolume: do {
                currentVolume = minVolume + 0.0625
            }
            case 1: do {
                currentVolume = 0.9375
            }
            default: break
            }

            
            if volume > currentVolume {
                // Volume up
            }
            if volume < currentVolume {
                // Volume down
            }

            parent.updateUIView(volume: volume)
            currentVolume = volume
        }

        func volumeControlIOS15(_ notification: NSNotification) {
            let minVolume: Float = 0.0625

            if let volume = notification.userInfo!["Volume"] as? Float {
                //avoiding duplicate events if same ID notification was generated
                if let seqN = self.lastVolumeNotificationSequenceNumber {
                    if seqN == notification.userInfo!["SequenceNumber"] as! Int {
                        // Duplicate nofification received
                    }
                    else {
                        self.lastVolumeNotificationSequenceNumber = (notification.userInfo!["SequenceNumber"] as! Int)
                        manageVolume(volume: volume, minVolume: minVolume)
                    }
                }
                else {
                    self.lastVolumeNotificationSequenceNumber = (notification.userInfo!["SequenceNumber"] as! Int)
                    manageVolume(volume: volume, minVolume: minVolume)
                }
            }
        }
    }
    
    let viewController = UIViewController()

    func makeUIViewController(context: Context) -> UIViewController {
        let volumeView = MPVolumeView(frame: CGRect.zero)
        volumeView.isHidden = true
        viewController.view.addSubview(volumeView)
        let childView = UIHostingController(rootView: builder(AVAudioSession.sharedInstance().outputVolume))
        addChildViewController(childView, to: viewController)
        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}

    private func addChildViewController(_ child: UIViewController, to parent: UIViewController) {

        if parent.children.count > 0{
            let viewControllers:[UIViewController] = parent.children
            for viewContoller in viewControllers{
                viewContoller.willMove(toParent: nil)
                viewContoller.view.removeFromSuperview()
                viewContoller.removeFromParent()
            }
        }

        parent.addChild(child)
        child.view.translatesAutoresizingMaskIntoConstraints = false
        parent.view.addSubview(child.view)
        child.didMove(toParent: parent)
        child.view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0)
        NSLayoutConstraint.activate([
            child.view.leadingAnchor.constraint(equalTo: parent.view.leadingAnchor),
            child.view.trailingAnchor.constraint(equalTo: parent.view.trailingAnchor),
            child.view.topAnchor.constraint(equalTo: parent.view.topAnchor),
            child.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor)
        ])
    }

    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator(self)

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

        return coordinator
    }

    func updateUIView(volume: Float) {
        let childView = UIHostingController(rootView: builder(volume))
        addChildViewController(childView, to: self.viewController)
    }
}

This will give you a VolumeEventReader that can be used in Swift as such:

struct ContentView: View { 
    var body: some View {
        VStack {
          VolumeEventReader { volume in
              VStack {
                  Text("Volume: \(volume)")
              }
              .onAppear {
                  print("\(volume)")
              }
          }
          Text("Hello World")
        }
    }
}

Note: you can put any View inside VolumeEventReader I was just giving an example with VStack. It was inspired by GeometryReader.

Credit to answers that led me to this solution:
System Volume Change Observer not working on iOS 15
Observing system volume in SwiftUI

Upvotes: 1

Dario Pellegrini
Dario Pellegrini

Reputation: 1768

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)

Upvotes: 2

Matheus Lima
Matheus Lima

Reputation: 178

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

Upvotes: 9

rakeshbs
rakeshbs

Reputation: 24572

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

Upvotes: 35

jnblanchard
jnblanchard

Reputation: 1200

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.

Upvotes: 5

Matthijs
Matthijs

Reputation: 535

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

Upvotes: 22

Related Questions