通过UIPanGesture控制设备音量:手势结束后音量指示器抖动问题
嘿,这个音量指示器抖动的问题我之前做类似功能时也踩过坑,咱们来一步步拆解解决~
首先得明确问题根源:你设置了isDragging但还是抖,大概率是手势状态的处理不完整,或者系统音量通知的回调时机和你的拖拽状态判断有错位。下面是具体的排查和解决步骤:
1. 补全手势状态的isDragging赋值逻辑
UIPanGestureRecognizer的状态不止.began和.ended,还有.cancelled、.failed这些场景(比如手势中途被其他控件拦截)。如果只在.ended时把isDragging设为false,就会出现状态遗漏,导致后续的系统通知还是被误判成拖拽中。
修正后的手势处理代码:
@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) { switch gesture.state { case .began: isDragging = true case .changed: // 这里是你的音量调整逻辑,比如根据位移计算音量变化 let translation = gesture.translation(in: self.view) adjustVolume(with: translation) // 重置位移避免累积,让每次变化都基于当前手势增量 gesture.setTranslation(.zero, in: self.view) case .ended, .cancelled, .failed: isDragging = false default: break } }
2. 在音量通知回调里严格拦截拖拽状态
系统的AVSystemController_SystemVolumeDidChangeNotification有时候会有延迟触发的情况,比如手势结束后,系统音量的同步还会发一次通知。这时候必须在回调里第一时间判断isDragging,直接跳过拖拽中的通知:
@objc func handleVolumeChanged(_ notification: Notification) { // 拖拽中完全忽略系统通知,用自己的逻辑更新指示器 guard !isDragging else { return } // 非拖拽状态下,再同步系统音量到你的指示器 if let volume = notification.userInfo?["AVSystemController_AudioVolumeNotificationParameter"] as? Float { updateVolumeIndicator(with: volume) } }
3. 拖拽过程中直接控制指示器,不依赖系统通知
最稳妥的方式是:拖拽时完全用自己的逻辑更新自定义指示器,手势结束后再同步系统音量。这样系统通知就不会干扰拖拽过程中的指示器状态,从根源避免抖动。
完整的示例代码片段:
import UIKit import MediaPlayer import AVFoundation class MainVC: UIViewController { lazy var sideVolumeIndicator: UIView = { let view = UIView() view.backgroundColor = .green view.translatesAutoresizingMaskIntoConstraints = false return view }() private var isDragging = false private var currentVolume: Float = 0.5 // 初始音量可从系统获取 override func viewDidLoad() { super.viewDidLoad() setupViews() setupGesture() setupVolumeNotification() // 初始化系统音量 currentVolume = AVAudioSession.sharedInstance().outputVolume updateVolumeIndicator() } private func setupViews() { view.addSubview(sideVolumeIndicator) // 给指示器加约束:右侧固定,垂直居中,宽度固定,高度随音量变化 NSLayoutConstraint.activate([ sideVolumeIndicator.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), sideVolumeIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), sideVolumeIndicator.widthAnchor.constraint(equalToConstant: 6), sideVolumeIndicator.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: currentVolume) ]) } private func setupGesture() { let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) view.addGestureRecognizer(panGesture) } private func setupVolumeNotification() { NotificationCenter.default.addObserver( self, selector: #selector(handleVolumeChanged(_:)), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil ) } @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { switch gesture.state { case .began: isDragging = true case .changed: let translation = gesture.translation(in: view) // 根据垂直位移计算音量变化:向上滑增大,向下滑减小 let volumeDelta = -translation.y / view.bounds.height * 1.0 currentVolume = max(0.0, min(1.0, currentVolume + volumeDelta)) // 直接更新自定义指示器,不用等系统通知 updateVolumeIndicator() gesture.setTranslation(.zero, in: view) case .ended, .cancelled, .failed: isDragging = false // 手势结束后再同步系统音量 syncSystemVolume() default: break } } @objc private func handleVolumeChanged(_ notification: Notification) { guard !isDragging else { return } if let volume = notification.userInfo?["AVSystemController_AudioVolumeNotificationParameter"] as? Float { currentVolume = volume updateVolumeIndicator() } } private func updateVolumeIndicator() { // 动态更新指示器高度,用小动画保证平滑 sideVolumeIndicator.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: CGFloat(currentVolume)).isActive = true UIView.animate(withDuration: 0.1) { self.view.layoutIfNeeded() } } private func syncSystemVolume() { // iOS 13+ 无法直接设置AVAudioSession的outputVolume,用MPVolumeView间接设置 let volumeView = MPVolumeView() if let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider { slider.value = currentVolume } } }
额外注意点
- iOS 13及以上版本,
AVAudioSession.sharedInstance().outputVolume是只读属性,必须通过MPVolumeView的滑块来设置系统音量 - 如果你的指示器用了动画,要保证手势结束时的动画是一次性完成的,避免残留的动画队列导致抖动
- 记得在控制器销毁时移除通知观察者,避免内存泄漏
内容的提问来源于stack exchange,提问作者PrepareFor
相关产品推荐
相关产品推荐

