iOS 9: How to change volume programmatically without showing system sound bar popup?
Asked Answered
N

12

25

I have to change the volume on iPad and using this code:

[[MPMusicPlayerController applicationMusicPlayer] setVolume:0];

But this changing volume and showing system volume bar on iPad. How to change the sound without showing the volume bar?

I know, setVolume: is deprecated, and everybody says to use MPVolumeView. If this is the only way to solve my problem, then how to change the volume using MPVolumeView? I don't see any method in MPVolumeView that changes the sound.
Should I use some another class together with MPVolumeView?

But it's preferable to use MPMusicPlayerController.

Thank you for advice!

Nocuous answered 16/10, 2015 at 10:41 Comment(2)
You're not supposed to change the volume programmatically, that's the whole point of setVolume: being deprecated. You app could be rejected.Behalf
whats about SWIFTUI? How can we do it? https://mcmap.net/q/63301/-swiftui-change-sound-levelDemantoid
T
20

MPVolumeView has a slider, and by changing the value of the slider, you can change the device volume. I wrote an MPVolumeView extension to easily access the slider:

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
    }
}
Tic answered 23/10, 2015 at 16:52 Comment(7)
Did this work? I am trying to use it without much success! I added a view and subclassed it to MPVolumeView but it doesn't seem to be working.Troublemaker
Work for me! Thx!Karinekariotta
this was super usefulPoliomyelitis
I dont have problems on iOS 11.3. I do on iOS 11.4 @OttoBoyMccarter
@Karl-JohnChow Just tested and you are right. Before iOS 11.4 is working well, on iOS 11.4 it doesn't.Backsight
@Karl-JohnChow I asked on SO, waiting maybe someone could help: #50738443Backsight
@Karl-JohnChow I added working solution on iOS 11.4 here #33168997Anabolite
A
77

For 2018, working on iOS 11.4

You need to change slider.value after a small delay.

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
    }
  }
}

Usage:

MPVolumeView.setVolume(0.5)

Objective-C version

Anabolite answered 7/6, 2018 at 11:38 Comment(15)
In my case it's working in iOS 11.4 only setting a much longer deadline (e.g., DispatchTime.now() + 0.5)Brochu
@AndreaGorrieri It still works with 0.01 delay without showing system sound bar. Seem like maybe you have another additional problem here. Anyway, upvoted your comment for someone else has same issue. Thanks!Anabolite
This works for me, thanks. But each time the volume is increased, I get the following log: [] <<<< AVOutputDeviceDiscoverySession (FigRouteDiscoverer) >>>> -[AVFigRouteDiscovererOutputDeviceDiscoverySessionImpl outputDeviceDiscoverySessionDidChangeDiscoveryMode:]: Setting device discovery mode to DiscoveryMode_None Has anyone been able to get rid of that message?Napiform
@Napiform As I know, this warning appears whenever MPVolumeView is initialized (from xib, storyboard or programmatically). Currently, seem like it still works good with this warning and no one has solution to avoid warning in this case.Anabolite
@SimonPham I tried on iOS 12.0 and it works as expected. Make sure you run on real device, not simulator. If it still doesn't work, you can try to increase delay time as AndreaGorrieri comment above.Anabolite
@Anabolite maybe it's because I call the function when the app is in background. Does it need to be on foreground? I use real device. I increased the delay to 0.5 but still doesn't workIrk
@SimonPham Of course, you have to call it on foreground.Anabolite
do you have any solution for my problem? I need to call it when the app comes to background.Irk
@SimonPham Actually changing volume programmatically is updating an UIKit component which is UISlider. Your question will become how to update UI while app is in background. I think it's impossible.Anabolite
@Anabolite I'm unable to get this to work on iOS 12, the system HUD still shows for me! Could you post an exact code sample?Normi
Perfect example!Sunk
This wasn't working for me (running iOS 12 and swift 4) and then I increased the deadline like @AndreaGorrieri did and it works fine :).Trichome
This is a great solution. What is the need to use DispatchQueue.main.asyncAfter in setting and getting the volume? I rewrote this extension without it and it still works.. unless I am missing something.Asparagine
This code is flawed — it creates a brand new MPVolumeView UI object every time you set the volume. I assume the async wait is needed because it could take the system a while to initialize this new object. I recommend creating the MPVolumeView once at launch — then use it anytime without a wait and without wasting battery.Airliah
Working for iOS 13 and 13+ as well.Solitta
T
20

MPVolumeView has a slider, and by changing the value of the slider, you can change the device volume. I wrote an MPVolumeView extension to easily access the slider:

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
    }
}
Tic answered 23/10, 2015 at 16:52 Comment(7)
Did this work? I am trying to use it without much success! I added a view and subclassed it to MPVolumeView but it doesn't seem to be working.Troublemaker
Work for me! Thx!Karinekariotta
this was super usefulPoliomyelitis
I dont have problems on iOS 11.3. I do on iOS 11.4 @OttoBoyMccarter
@Karl-JohnChow Just tested and you are right. Before iOS 11.4 is working well, on iOS 11.4 it doesn't.Backsight
@Karl-JohnChow I asked on SO, waiting maybe someone could help: #50738443Backsight
@Karl-JohnChow I added working solution on iOS 11.4 here #33168997Anabolite
M
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()
      }
    }
  }
}

Usage:

// set volume to 50%
viewController.setVolume(0.5)
Manchineel answered 11/6, 2017 at 6:5 Comment(0)
W
5

Here's a solution in Swift. It might be a shady one, so I'll let you know if Apple approved this when I publish. Meantime, this works just fine for me:

  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
        }
    }
    

Important note: This solution will prevent the iPhone's Volume HUD from appearing - either when you change the volume in your code, or when the user clicks the external volume buttons. If you do want to display the HUD, then skip all the hidden view stuff, and don't add the MPVolumeView as a subview at all. This will cause iOS to display the HUD when the volume changes.

Worthy answered 31/7, 2016 at 19:19 Comment(0)
A
5

Swift 5 / iOS 13

Here is the most reliable way I have found to access the system volume level. This is based on other answers here but does not waste energy nor require an async wait.

During initialization (such as in viewDidLoad), create an off-screen 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
}

Then use the hidden slider to get or set the system volume whenever you need it:

var systemVolume:Float {
    get {
        return hiddenSystemVolumeSlider.value
    }
    set {
        hiddenSystemVolumeSlider.value = newValue
    }
}
Airliah answered 12/7, 2020 at 1:43 Comment(1)
This is the only method that I could get to work in 9/2023. Is this still the best method for setting the volume? Is there a way to not call up the system volume slider?Verse
G
4

I don't think there is any way to change the volume without flashing volume control. You should use MPVolumeView like this:

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];
Grimbly answered 16/10, 2015 at 11:31 Comment(1)
This works for me on iOS8 but does NOT work on iOS9. Is there a iOS9 workaround?Verret
C
3

@udjat 's answer in 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
    }
}
Crocus answered 20/2, 2017 at 10:23 Comment(0)
K
2

Version: 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
    }
}
Karinekariotta answered 13/12, 2016 at 8:54 Comment(2)
how are you supposed to use this?Kemper
Hi @JonathanPlackett, you can try like this: self.volumeSlider.value = 1 // get the max volume BTW, self means MPVolumeView instance. Hope for help.Karinekariotta
V
2

Here is my volume control for my audio player app:

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
        }
    }
}

How to connect:

  1. Set UIVolumeSlider class in Identity Inspector on storyboard:

enter image description here

  1. Connect instance to class:
@IBOutlet private var volumeSlider:UIVolumeSlider!
  1. In viewDidLoad method:
volumeSlider.updatePositionForSystemVolume()
  1. Activate and deactivate it in didAppear and willDisappear:
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        volumeSlider.activate()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        volumeSlider.deactivate()
        super.viewWillDisappear(animated)
    }

Velarize answered 5/2, 2020 at 7:28 Comment(2)
use viewWillAppear instead of viewDidAppear.Cannular
very bad - "outputVolume" string literal multiple times, UI merged with logic (so I can't for example take your code and use it with AVPlayer instead of MediaPlayer) and etc.Springing
N
1

Swift > 2.2, iOS > 8.0,

I didn't find any solution I was looking but I end up doing this as solution:

let volumeView = MPVolumeView()

override func viewDidLoad() {
    ...
    view.addSubview(volumeView)
    volumeView.alpha = 0.00001
}

func changeSpeakerSliderPanelControls(volume: Float) {
    for subview in self.volumeView.subviews {

        if subview.description.rangeOfString("MPVolumeSlider") != nil {
             let slider = subview as! UISlider
             slider.value = volume

             break
        }
    }
}
Nivernais answered 23/6, 2016 at 1:30 Comment(0)
J
1

You can use default UISlider with this code:

    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
       }
    }

next in code just write

systemSlider.value = customSlide.value
Japan answered 2/4, 2018 at 9:20 Comment(0)
W
-1

I didn't want to have to have my call within a view controller so I built a different solution. You can just call it from anywhere like this:

SystemVolumeController.shared.systemVolume = 0.5

One thing to keep in mind with all the MPVolumeView solutions is while the MPVolumeView is on screen the little slider that appears at the side of the screen when a user presses the volume buttons on the side of the phone doesn't appear. For this reason I built into this solution a timer that removes the view after a few seconds.

Some of the solutions here suggest you need to set a delay before setting the volume. I haven't found that to be the case for me but your mileage may vary.

import MediaPlayer

final class SystemVolumeController {
    
    static let shared: SystemVolumeController = SystemVolumeController()
    
    private weak var volumeView: MPVolumeView?
        
    private var removeTimer: Timer?
    
    private func getVolumeView() -> MPVolumeView? {
        if let volumeView = volumeView {
            return volumeView
        }
        else {
            removeTimer?.invalidate()
            
            let volumeView = MPVolumeView(frame: CGRect(x: -CGFloat.greatestFiniteMagnitude, y:0, width:0, height:0))
            guard let window = UIApplication.shared.windows.first else {
                assertionFailure("No window to add the volume slider to")
                return nil
            }
            window.addSubview(volumeView)
            self.volumeView = volumeView
            
            removeTimer = Timer.scheduledTimer(withTimeInterval: 3,
                                               repeats: false,
                                               block: { _ in
                volumeView.removeFromSuperview()
            })
            
            return volumeView
        }
    }
        
    private var volumeSlider: UISlider? {
        getVolumeView()?.subviews.first(where: { $0 is UISlider }) as? UISlider
    }
    
    var systemVolume: Float {
        get {
            AVAudioSession.sharedInstance().outputVolume
        }
        set {
            volumeSlider?.value = newValue
        }
    }
    
    private init() {}
    
}```
Wilbanks answered 15/1, 2022 at 20:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.