我使用以下代码来检测用户更改的系统音量。
NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
当我更新到 iOS 15 时,我发现这段代码不起作用,但对于任何以前版本的 iOS 来说它都有效。
我还使用了
addObserver
功能,但没关系。
这是 iOS 15 的错误吗?如果是,我能做些什么来修复它。
谢谢:)
我挂钩了MPVolumeControllerSystemDataSource的方法_systemVolumeDidChange,并且在iOS 15.0(至少beta2),通知名称已更改为SystemVolumeDidChange,这是新的通知结构:
{
AudioCategory = "Audio/Video";
Reason = ExplicitVolumeChange;
SequenceNumber = 1069;
Volume = 0;
}
有两点需要注意:
您正在做的事情不受支持,因此如果它不能在所有系统上运行也不足为奇。正确记录的方法是在音频会话
outputVolume
属性上使用 KVO:https://developer.apple.com/documentation/avfaudio/avaudiosession/1616533-outputvolume
我也在努力解决处理音量按钮按下事件的任务,旨在
对我有用的最终解决方案(根据 2022 年 9 月)如下:
在我的视图控制器中
var lastVolumeNotificationSequenceNumber: Int? //see below explanations - avoiding duplicate events
var currentVolume: Float? //needed to remember your current volume, to properly react on up/down events
在我的 loadView 函数中:
if #available(iOS 15, *) {
NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged(_:)), name: NSNotification.Name(rawValue: "SystemVolumeDidChange"), object: nil)
}
else {
NotificationCenter.default.addObserver(self, selector: #selector(volumeChanged(_:)), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
}
#available 标签允许您选择根据 iOS 版本设置的通知。
...我的视图控制器有这个:
@objc func volumeChanged(_ notification: NSNotification) {
DispatchQueue.main.async { [self] in
if #available(iOS 15, *) {
volumeControlIOS15(notification)
}
else {
volumeControlIOS14(notification)
}
}
}
这个是处理事件本身以及区分14/15版本的代码(略有不同)
请注意:这里使用 DispatchQueue.main.async,一旦完成处理程序(如上所述)不在主线程上,而在我的情况下它必须在主线程上。在我弄清楚之前,我遇到了一些崩溃和线程警告。
func manageVolume(volume: Float, minVolume: Float) {
switch volume {
case minVolume: do {
currentVolume = minVolume + 0.0625
}
case 1: do {
currentVolume = 0.9375
}
default: break
}
if let cV = currentVolume {
if volume > cV {
//do your stuff here
}
if volume < cV {
//do your stuff here
}
currentVolume = volume
}
else {
currentVolume = volume
}
}
此函数用于处理音量按钮按下事件,还可以帮助您 a) 了解事件是“向上”还是“向下”,以及 b) 管理达到最大/最小值的情况(如果您需要继续)即使您触及最大/最小,也可以进行事件处理(这是通过简单地减少/增加当前音量变量来完成的 - 请记住,您无法更改系统音量本身,但是,该变量都是您的;))
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 {
NSLog("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)
}
}
}
它是iOS 15的主要实现函数。正如你所看到的,minVolume 不是一个数字,它是一个 let 常量 - 它与 iOS 14 不同(我发现在 iOS 14 上它是 0,而 iOS 15 在我的物理设备上不会低于 0.0625 - 请不要问我为什么,这是一个谜;))
它还处理最后一个通知唯一 ID 并省略重复的通知事件,这在 iOS15 中(不知何故)很常见。
func volumeControlIOS14(_ notification: NSNotification) {
//old implementation for iOS < 15
let minVolume: Float = 0
if let volume = notification.userInfo!["AVSystemController_AudioVolumeNotificationParameter"] as? Float {
manageVolume(volume: volume, minVolume: minVolume)
}
}
iOS 14 也是如此,有 3 个主要区别:a) 通知 UserInfo 键,如上所述,不同 b) 没有重复的通知控件 - 只要我在 iOS 14 上观察到没有重复,c) minVolume 是0,这对于 iOS 14 是正确的
希望对您有帮助:)
尝试了 AdamWang 的答案后,我发现您需要创建并保留 MPVolumeView 的实例(但不需要添加到视图层次结构中),否则将不会发出通知。
如果有人突然不明白如何应用AdamWang的解决方案,你只需要将“
AVSystemController_SystemVolumeDidChangeNotification
”替换为“SystemVolumeDidChange
”即可。
在 iOS15 中,不再调用@“AVSystemController_SystemVolumeDidChangeNotification”通知。
使用键值观察来代替。 (扩展上面马特的答案)
在你的 ViewController.m 文件中
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>
@interface ViewController : UIViewController
{
AVAudioSession *audioSession;
}
@end
在您的 View Controller.m 文件中
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
audioSession = [AVAudioSession sharedInstance];
[audioSession setActive:YES error:nil];
[audioSession addObserver:self forKeyPath:@"outputVolume" options:0 context:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[audioSession removeObserver:self forKeyPath:@"outputVolume"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
CGFloat newVolume = audioSession.outputVolume;
NSLog(@"newVolume: %f", newVolume);
//if the volume gets to max or min observer won't trigger
if (newVolume > 0.9 || newVolume < 0.1) {
[self setSystemVolume:0.5];
return;
}
}
//set the volume programatically
- (void)setSystemVolume:(CGFloat)volume {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[MPMusicPlayerController applicationMusicPlayer] setVolume:(float)volume];
#pragma clang diagnostic pop
}
您可以使用移出屏幕的 MPVolumeView 来隐藏音量滑块。
我尝试了上面安德烈的解决方案。它有效,但我不断收到重复的事件,有时甚至在不改变音量的情况下收到额外的事件。 这是我的代码,考虑应用程序阶段。
class VolumeObserver {
var volume: Float = 0
private var volumeObserver: NSKeyValueObservation?
let volumeView = MPVolumeView(frame: .zero)
var callback:()->Void = {}
init(_ back:@escaping ()->Void){
callback = back
observeVolumeChanges()
NotificationCenter.default.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil)
}
func observeVolumeChanges() {
try! AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default, options: [])//Prevent the app stop the current playing media
let audioSession = AVAudioSession.sharedInstance()
try! audioSession.setActive(true)
volume = audioSession.outputVolume
volumeObserver = audioSession.observe(\.outputVolume) { [weak self] (audioSession, _) in
guard let self = self else { return }
let newVolume = audioSession.outputVolume
if abs(newVolume - self.volume ) > 0.01 {
self.volume = newVolume
callback()
}
}
}
@objc func appMovedToBackground() {
volumeObserver?.invalidate()
}
@objc func appMovedToForeground() {
observeVolumeChanges()
}//it's important to remove and re-register after the app's phase changes.
}