System Volume Change Observer Not Working on iOS 15

System Volume Change Observer not working on iOS 15

What you're doing is unsupported, so it's not really surprising if it doesn't work on all systems. The correct documented approach is to use KVO on the audio session outputVolume property: https://developer.apple.com/documentation/avfaudio/avaudiosession/1616533-outputvolume

Alternative to AVSystemController_AudioVolumeNotificationParameter post iOS 15?

Three cheers for open source, specifically: JPSVolumeButtonHandler. This component works like a champ, and uses the Apple-approved KVO technique. Be aware that this component sets AVAudioSession options to .mixWithOthers which prevents MPRemoteCommandCenter from receiving/handing any BlueTooth commands. So if you need BT (Swift 5):

let volumeButtonHandler = JPSVolumeButtonHandler(up: {
// handle up press
}, downBlock: {
// handle down press
})
volumeButtonHandler.sessionOptions = [] // allow remote BT

I also found that programmatically setting the device volume to 0.5 before initializing the button handler avoided occasional min/max barriers. If the device initial volume was close to the min or max, the handler would stop after a few button presses:

try AVAudioSession.sharedInstance().setActive(true, options: [])
MPVolumeView(frame: .zero).volumeSlider.value = 0.5

SystemVolumeDidChangeNotification never get triggered on iOS 14

beginReceivingRemoteControlEvents need to be called first.

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

UIApplication.shared.beginReceivingRemoteControlEvents() // <- THIS LINE

NotificationCenter.default.addObserver(self, selector: #selector(volumeChange(_:)), name: Notification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
}

Do not forget to remove the observer after the view disappeared.

AVSystemController_SystemVolumeDidChangeNotification triggers when device locked

Wound up manually keeping the volume constant and using the audio session method. Had to throw in a couple hacks. This is a bit convoluted, so I'm open to cleaner alternatives. No idea how Apple would react to this being submitted in an app, although it seems that they definitely accept apps that use the volume buttons for interacting with the cameras.

Inside a UIViewController subclass:

override func viewDidLoad() {
super.viewDidLoad()
// …
setupVolumeButton()
}

private let volumeView = MPVolumeView(frame: CGRect(x: 0, y: -100, width: 0, height: 0)) // override volume view

private func setupVolumeButton() {
view.addSubview(volumeView)

setVolume(0.5) { // in case app launches with volume at max/min already
// need to wait until initial volume setting is done
// so it doesn't get triggered on launch

let audioSession = AVAudioSession()
try? audioSession.setActive(true)
audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
}
}

private func setVolume(_ volume: Float, completion: (() -> Void)? = nil) {
let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
slider?.value = volume

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
// needed to wait a bit before completing so the observer doesn't pick up the manualq volume change
completion?()
}
}
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "outputVolume" {
setVolume(0.5) // keep from reaching max or min volume so button keeps working

doTheThingThatShouldHappenWhenTheVolumeButtonIsPressed()
}
}

Edit: Also I noticed that the audio session was deactivated when the app was closed, so I stored the audio session in a property, added an observer for when the app became active again, and in the associated method I set the audio session to be active again.

Detect hardware volume button press when volume not changed


let volumeView = MPVolumeView(frame: CGRect.zero)
self.view.addSubview(volumeView)
NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged(_:)), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)

This will get called every press regardless of volume level

@objc func volumeChanged(_ notification: NSNotification) {
if let volume = notification.userInfo!["AVSystemController_AudioVolumeNotificationParameter"] as? Float {
print("volume: \(volume)")
}
}

output:

volume: 0.8125
volume: 0.875
volume: 0.9375
volume: 1.0
volume: 1.0
volume: 1.0
volume: 1.0

Swift: Detect Volume Button Press NOT volume change events

So what I'd do is subscribe to the notification like they do in the questions you linked and then add a variable named say programmaticVolumeChange. When you change the volume programmatically set the variable to true, then in your function observeValueForKeyPath, if the variable is true don't cancel the alarm (and naturally set it to false after). That way when the user presses the volume buttons you know it wasn't your code. Maybe test the amount of time between a programmatic volume change and the function call but I think that would be fine.

Eg

var programmaticVolumeChange = false

func changeVolumeProgramatically() {

programmaticVolumeChange = true
//change volume programmatically after

}

override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject,
change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {

if keyPath == "outputVolume"{

if programmaticVolumeChange {

programmaticVolumeChange = false

} else {

//handle logic for user volume button press

}

}

}


Related Topics



Leave a reply



Submit