Swift如何在VC首次展示时检测耳机是否已插入
问题根因
AVAudioSession.routeChangeNotification 只会在音频路由发生实际变更的瞬间触发,VC首次加载、音频会话首次激活的过程中如果没有发生路由切换,系统不会投递这个通知。仅靠注册通知监听,只能捕获注册完成之后发生的耳机插拔事件,无法获取注册前就已经存在的耳机插入状态,这就是首次加载时检测失效的核心原因。
修复方案
核心逻辑是:不要完全依赖通知回调获取耳机状态,在完成音频会话激活、注册路由监听之后,主动查询一次当前的音频路由,做初始状态校验,同时抽离通用的状态判断、UI更新逻辑,供初始检测和通知回调复用。
1. 抽离通用状态检测方法
把耳机状态判断逻辑从通知回调中独立出来,方便多处调用:
/// 检测当前是否有耳机类设备接入 func isHeadphoneConnected() -> Bool { let currentOutputs = AVAudioSession.sharedInstance().currentRoute.outputs for output in currentOutputs { // 可根据业务需求调整需要识别的端口类型,以下覆盖常见有线/蓝牙耳机 switch output.portType { case .headphones, .bluetoothHFP, .bluetoothA2DP: return true default: continue } } return false }
2. 抽离UI更新逻辑
把状态变更后的UI操作独立,保证初始加载和后续路由变更的UI表现一致:
func updateHeadphoneRelatedUI(isConnected: Bool) { if isConnected { print("耳机已接入") view.backgroundColor = .blue } else { print("耳机未接入") view.backgroundColor = .white } }
3. 修正初始化流程
去掉重复的通知注册逻辑,在音频会话激活成功后,完成监听注册+初始状态检测:
var didSetupAudioSession = false override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 保证音频会话初始化逻辑只执行一次 guard !didSetupAudioSession else { return } didSetupAudioSession = true do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playAndRecord, mode: .default, options: [ .duckOthers, .allowBluetoothA2DP, .allowAirPlay, .mixWithOthers, .defaultToSpeaker ]) try session.setActive(true) // 注册路由变化监听 setupRouteChangeObserver() // 主动检测初始耳机状态 updateHeadphoneRelatedUI(isConnected: isHeadphoneConnected()) } catch { print("音频会话初始化失败: \(error.localizedDescription)") } } func setupRouteChangeObserver() { NotificationCenter.default.addObserver( self, selector: #selector(handleAudioRouteChange), name: AVAudioSession.routeChangeNotification, object: nil ) } deinit { // 移除观察者,避免内存泄漏 NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil) }
4. 简化通知回调逻辑
路由变更时直接调用通用检测方法,不要在回调内重复写状态判断逻辑,避免逻辑不一致:
@objc private func handleAudioRouteChange(notification: NSNotification) { guard let userInfo = notification.userInfo, let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, let changeReason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } // 仅处理设备接入、移除两类场景,其余路由变化忽略 switch changeReason { case .newDeviceAvailable, .oldDeviceUnavailable: updateHeadphoneRelatedUI(isConnected: isHeadphoneConnected()) default: break } }
注意事项
- 不要重复注册路由监听,原代码在
viewDidLayoutSubviews和viewDidAppear中两次调用注册方法,会导致同一次路由变化触发多次回调,只需要在音频会话激活成功后注册一次即可。 - 初始状态检测必须在
AVAudioSession激活成功之后执行,否则拿到的currentRoute信息可能不准确。 - 如果需要区分AirPods、有线耳机、蓝牙音箱等不同设备类型,只需要调整
isHeadphoneConnected方法中的端口类型判断规则即可。
内容的提问来源于stack exchange,提问作者Lance Samaria
相关产品推荐
相关产品推荐

