Reputation: 2026
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
Reputation: 2997
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
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
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
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
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
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