iOS 9: 如何在不显示系统声音栏弹出窗口的情况下以编程方式更改音量?

25

我需要在iPad上调整音量,使用了以下代码:

[[MPMusicPlayerController applicationMusicPlayer] setVolume:0];

但是这会改变音量并在iPad上显示系统音量条。 如何在不显示音量条的情况下更改声音?

我知道,setVolume:已经被弃用了,大家都说要使用MPVolumeView。如果这是解决我的问题的唯一方法,那么如何使用MPVolumeView更改音量呢?我没有看到MPVolumeView中任何可以更改音量的方法。
我应该与MPVolumeView一起使用其他类吗?

但最好使用MPMusicPlayerController

谢谢您的建议!


1
你不应该以编程的方式更改音量,这就是“setVolume:”被弃用的全部意义。你的应用可能会被拒绝。 - Alejandro Iván
1
关于SWIFTUI怎么样?我们该怎么做呢?https://stackoverflow.com/questions/60681324/swiftui-change-sound-level - user12037622
12个回答

76

在2018年,我们正在使用iOS 11.4系统。

您需要稍微延迟后更改slider.value

extension MPVolumeView {
  static func setVolume(_ volume: Float) {
    let volumeView = MPVolumeView()
    let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider

    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
      slider?.value = volume
    }
  }
}

使用方法:

MPVolumeView.setVolume(0.5)

Objective-C版本


14
在我的情况下,仅在iOS 11.4上设置更长的截止时间(例如,DispatchTime.now() + 0.5),才能正常工作。 - Andrea Gorrieri
4
即使没有显示系统声音栏,并且延迟为0.01,它仍然有效。好像你在这里还有另一个额外的问题。无论如何,我赞同你的评论,因为其他人可能也遇到了同样的问题。谢谢! - trungduc
2
据我所知,每当从xib、storyboard或编程方式初始化MPVolumeView时,就会出现此警告。目前看来,即使有这个警告,它仍然可以正常工作,并且没有人能够解决在这种情况下避免警告的问题。 - trungduc
1
这是一个很好的解决方案。在设置和获取音量时,为什么需要使用DispatchQueue.main.asyncAfter?我重写了这个扩展程序而没有使用它,它仍然可以工作...除非我漏掉了什么。 - daspianist
4
这段代码有缺陷 - 每次设置音量时都会创建一个全新的MPVolumeView UI对象。我猜异步等待是必要的,因为系统可能需要一段时间来初始化这个新对象。我建议在启动时只创建一次MPVolumeView - 然后随时使用它,无需等待和浪费电池。 - Robin Stewart
显示剩余10条评论

20

MPVolumeView有一个滑块,通过改变滑块的值,可以改变设备音量。我编写了一个MPVolumeView扩展,以便轻松访问滑块:

extension MPVolumeView {
    var volumeSlider:UISlider {
        self.showsRouteButton = false
        self.showsVolumeSlider = false
        self.hidden = true
        var slider = UISlider()
        for subview in self.subviews {
            if subview.isKindOfClass(UISlider){
                slider = subview as! UISlider
                slider.continuous = false
                (subview as! UISlider).value = AVAudioSession.sharedInstance().outputVolume
                return slider
            }
        }
        return slider
    }
}

这个有用吗?我尝试使用它但没有成功!我添加了一个视图并将其子类化为MPVolumeView,但似乎不起作用。 - Gugulethu
1
这非常有用。 - Fluidity
我在iOS 11.3上没有问题,但在iOS 11.4上有问题。@OttoBoy - Karl-John Chow
@Karl-JohnChow 刚刚测试了一下,你是对的。在 iOS 11.4 之前可以正常工作,在 iOS 11.4 上就不行了。 - iOS Dev
1
@Karl-JohnChow 我在这里添加了适用于iOS 11.4的工作解决方案 https://dev59.com/clwX5IYBdhLWcg3w-DfJ#50740234 - trungduc
显示剩余2条评论

6
extension UIViewController {
  func setVolumeStealthily(_ volume: Float) {
    guard let view = viewIfLoaded else {
      assertionFailure("The view must be loaded to set the volume with no UI")
      return
    }

    let volumeView = MPVolumeView(frame: .zero)

    guard let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider else {
      assertionFailure("Unable to find the slider")
      return
    }

    volumeView.clipsToBounds = true
    view.addSubview(volumeView)

    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { [weak slider, weak volumeView] in
      slider?.setValue(volume, animated: false)
      DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { [weak volumeView] in
        volumeView?.removeFromSuperview()
      }
    }
  }
}

使用方法:

// set volume to 50%
viewController.setVolume(0.5)

5

这里提供一个Swift的解决方案。可能有点靠不住,如果我发布后被苹果批准了,我会让你知道的。与此同时,这个方法对我来说完全有效:

  1. Define an MPVolumeView and an optional UISlider in your View Controller

    private let volumeView: MPVolumeView = MPVolumeView()
    private var volumeSlider: UISlider?
    
  2. In the storyboard, define a view that's hidden from the user (height=0 should do the trick), and set an outlet for it (we'll call it hiddenView here). This step is only good if you want NOT to display the volume HUD when changing the volume (see note below):

    @IBOutlet weak var hiddenView: UIView!
    
  3. In viewDidLoad() or somewhere init-y that runs once, catch the UISlider that actually controls the volume into the optional UISlider from step (1):

    override func viewDidLoad() {
        super.viewDidLoad()
    
        ...
    
        hiddenView.addSubview(volumeView)
        for view in volumeView.subviews {
            if let vs = view as? UISlider {
                volumeSlider = vs
                break
            }
        }
    }
    
  4. When you want to set the volume in your code, just set volumeSlider?.value to be anywhere between 0.0 and 1.0, e.g. for increasing the volume:

    func someFunc() {
        if volumeSlider?.value < 0.99 {
            volumeSlider?.value += 0.01
        } else {
            volumeSlider?.value = 1.0
        }
    }
    

重要提示: 这个解决方案将防止iPhone音量界面出现,无论是在您的代码中更改音量还是用户点击外部音量按钮时。如果确实希望显示音量界面,则跳过所有隐藏视图内容,并且不要将MPVolumeView添加为子视图。这会导致iOS在音量更改时显示音量界面。


5

Swift 5 / iOS 13

以下是我发现的最可靠的访问系统音量级别的方法。该方法基于其他答案,但不会浪费能源,也不需要异步等待。

在初始化期间(例如在viewDidLoad中),创建一个离屏的MPVolumeView

var hiddenSystemVolumeSlider: UISlider!

override func viewDidLoad() {
    let volumeView = MPVolumeView(frame: CGRect(x: -CGFloat.greatestFiniteMagnitude, y:0, width:0, height:0))
    view.addSubview(volumeView)
    hiddenSystemVolumeSlider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
}

然后使用隐藏的滑块,在需要时获取或设置系统音量:

var systemVolume:Float {
    get {
        return hiddenSystemVolumeSlider.value
    }
    set {
        hiddenSystemVolumeSlider.value = newValue
    }
}

这是我在2023年9月找到的唯一可行的方法。现在这仍然是设置音量的最佳方法吗?有没有办法不调出系统音量滑块? - undefined

4

我认为没有办法在不刷音量控制的情况下改变音量。你应该像这样使用MPVolumeView

MPVolumeView* volumeView = [[MPVolumeView alloc] init];

// Get the Volume Slider
UISlider* volumeViewSlider = nil;

for (UIView *view in [volumeView subviews]){
    if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
        volumeViewSlider = (UISlider*)view;
        break;
    }
}

// Fake the volume setting
[volumeViewSlider setValue:1.0f animated:YES];
[volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];

1
这对我在iOS8上起作用,但在iOS9上不起作用。是否有iOS9的解决方法? - Frak

3

@udjat在Swift 3中的回答

extension MPVolumeView {
    var volumeSlider: UISlider? {
        showsRouteButton = false
        showsVolumeSlider = false
        isHidden = true
        for subview in subviews where subview is UISlider {
            let slider =  subview as! UISlider
            slider.isContinuous = false
            slider.value = AVAudioSession.sharedInstance().outputVolume
            return slider
        }
        return nil
    }
}

2

版本:Swift 3 和 Xcode 8.1

extension MPVolumeView {
    var volumeSlider:UISlider { // hacking for changing volume by programing
        var slider = UISlider()
        for subview in self.subviews {
            if subview is UISlider {
                slider = subview as! UISlider
                slider.isContinuous = false
                (subview as! UISlider).value = AVAudioSession.sharedInstance().outputVolume
                return slider
            }
        }
        return slider
    }
}

你应该如何使用它? - Jonathan Plackett
1
嗨@JonathanPlackett,你可以尝试这样做: self.volumeSlider.value = 1 // 获取最大音量 顺便说一下,self表示MPVolumeView实例。希望能帮到你。 - Jerome

2
这是我音频播放器应用程序的音量控制:
import UIKit
import MediaPlayer

class UIVolumeSlider: UISlider {

    private let keyVolume = "outputVolume"
    
    func activate(){
        updatePositionForSystemVolume()
        
        guard let view = superview else { return }
        let volumeView = MPVolumeView(frame: .zero)
        volumeView.alpha = 0.000001
        view.addSubview(volumeView)
        
        try? AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
        AVAudioSession.sharedInstance().addObserver(self, forKeyPath: keyVolume, options: .new, context: nil)
        
        addTarget(self, action: #selector(valueChanged), for: .valueChanged)
    }
    
    
    func deactivate(){
        AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: keyVolume)
        removeTarget(self, action: nil, for: .valueChanged)
        superview?.subviews.first(where: {$0 is MPVolumeView})?.removeFromSuperview()
    }
    
    
    func updatePositionForSystemVolume(){
        try? AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
        value = AVAudioSession.sharedInstance().outputVolume
    }
    
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == keyVolume, let newVal = change?[.newKey] as? Float {
            setValue(newVal, animated: true)
        }
    }
    
    
    @objc private func valueChanged(){
        guard let superview = superview else {return}
        guard let volumeView = superview.subviews.first(where: {$0 is MPVolumeView}) as? MPVolumeView else { return }
        guard let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider else { return }
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
            slider.value = self.value
        }
    }
}

如何连接:
1. 在故事板的身份检查器中设置UIVolumeSlider类。

enter image description here

将实例连接到类:
@IBOutlet private var volumeSlider:UIVolumeSlider!

viewDidLoad方法中:
volumeSlider.updatePositionForSystemVolume()

激活和禁用它在didAppear和willDisappear中:
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        volumeSlider.activate()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        volumeSlider.deactivate()
        super.viewWillDisappear(animated)
    }


2
使用viewWillAppear代替viewDidAppear。 - ikbal
非常糟糕 - "outputVolume" 字符串文字多次出现,界面与逻辑混合在一起(所以我不能例如拿你的代码来代替 MediaPlayer 使用 AVPlayer)等等。 - Gargo

1
您可以使用以下代码来使用默认的UISlider:

    import MediaPlayer

    class CusomViewCOntroller: UIViewController

    // could be IBOutlet
    var customSlider = UISlider()

    // in code
    var systemSlider =  UISlider()

    override func viewDidLoad() {
            super.viewDidLoad()

       let volumeView = MPVolumeView()
       if let view = volumeView.subviews.first as? UISlider{
          systemSlider = view
       }
    }

接下来在代码中写入:

systemSlider.value = customSlide.value

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接