因为项目需求需要通过调节手机音量键调节远程硬件设备音量,所以对iOS系统音量事件做了一些研究,也尝试了网上的一些方法,所以记录一些所见所得:
1. 通过KVO方式
监听AVAudioSession的OutputVolume属性,同时需要override observeValue方法
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true)
AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: [.new, .old], context: nil)
} catch {
//处理Error
}
这个方法的缺点在于当音量调到最小或者最大之后再向下或向上调节音量是不会产生事件的,不过也有work around的方法,也就是当音量到最小或最大时设置把系统音量再设置为一个相对大一点(0.0001)或者小一点(0.9999)的值。
let maxVolume: Float = 0.9999
let minVolume: Float = 0.0001
var systemVolume: Float?
var systemVolumeView: MPVolumeView?
var systemVolumeSlider: UISlider?
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "outputVolume" {
guard let newVolume = change?[.newKey] as? Float else {
return
}
guard let oldVolume = change?[.oldKey] as? Float else {
return
}
if newVolume == minVolume || newVolume == maxVolume {
return
}
//systemVolumeSlider会在后面有说明
if newVolume == 0 {
systemVolumeSlider?.setValue(minVolume, animated: false)
} else if newVolume == 1 {
systemVolumeSlider?.setValue(maxVolume, animated: false)
}
//做音量的比较处理
}
}
设置系统音量的方式不同版本也不太一样:
//iOS7以前可以通过
MPMusicPlayerController.applicationMusicPlayer.setVolume()
//iOS7之后可以通过MPVolumeView里的音量Slider来设置
systemVolumeView = MPVolumeView(frame: CGRect(x: 0, y: -200, width: 320, height: 100))
for view in systemVolumeView!.subviews {
if view.isKind(of: UISlider.self) {
systemVolumeSlider = view as? UISlider
break
}
}
}
//通过类似一下方式设置
let maxVolume: Float = 0.9999
let minVolume: Float = 0.0001
if systemVolume == 0 {
systemVolumeSlider?.setValue(minVolume, animated: false)
} else if systemVolume == 1 {
systemVolumeSlider?.setValue(maxVolume, animated: false)
}
不过以上的方法在iOS11下又会有问题,拿音量调小做说明:iOS11下调小音量的事件周期结束前是不允许设置的新值大于或等于旧值(0除外)。iOS音量键每触发一下音量调节0.0625,音量为0.0625时调低音量,按照上面的方法在observeValue的方法里会触发修改systemVolume的值到0.0001,这个时候修改会成功,音量会设置为0.0001,此时再往下调节音量音量会为0,而不会再被修改到0.0001,这个时候再调小就不会再产生事件了。
2. 通过监听Notification
监听 AVSystemController_SystemVolumeDidChangeNotification 事件
//替换前面AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: [.new, .old], context: nil)为
NotificationCenter.default.addObserver(self, selector: #selector(volumeNotification(_:)), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
//处理Notification
@objc func volumeNotification(_ notification: Notification) {
guard notification.name.rawValue == "AVSystemController_SystemVolumeDidChangeNotification" else {
return
}
guard let userInfo = notification.userInfo else {
return
}
guard let reason = userInfo["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String, reason == "ExplicitVolumeChange" else {
return
}
if let volumeNotification = userInfo["AVSystemController_AudioVolumeNotificationParameter"] as? Float {
//可以根据该值来做判断
//==0表示减小音量,==1表示调大音量,(0,1)时需要保存上一次的值做比较
}
}
这种方式在iOS11下也表现正常,不过也得注意代码中的过滤条件
关于隐藏系统的音量提示窗口
上面的两种方式都会显示系统的音量提示窗口,而在需求上我们其实是需要屏蔽掉的,这个时候我们就要借助于MPVolumeView。当window的subviews里存在MPVolumeView时系统音量提示窗口就不会显示,需要注意的是MPVolumeView不能是hidden的也不能alpha=0,所以我们需要让MPVolumeView在屏幕外
systemVolumeView = MPVolumeView(frame: CGRect(x: 0, y: -200, width: 320, height: 100))
UIApplication.shared.keyWindow?.insertSubview(systemVolumeView!, at: 0)
关于系统音量的恢复问题
还有一个问题在于我们在APP内调节了系统的音量,但退出APP时实际上是期望音量恢复到之前的大小,这个就需要我们注意在APP的生命周期做相应的处理。
applicationDidBecomeActive 记录当前音量并监听Notification
applicationWillResignActive 移除监听Notification并恢复之前记录的音量
func applicationWillResignActive(_ application: UIApplication) {
VolumeHelper.shared.removeObserve()
VolumeHelper.shared.restoreSystemVolume()
}
func applicationDidBecomeActive(_ application: UIApplication) {
VolumeHelper.shared.observeHardwareVolume()
}
//记录当前音量可以通过如下方式
systemVolume = AVAudioSession.sharedInstance().outputVolume
不过虽然如此还是处理不了APP crash或者人为杀掉情况下的系统音量恢复。
说明
代码都是示意,具体实现大家可以仁者见仁智者见智。