Search code examples
iosswiftswiftuiuikit

How to hide iOS volume indicator?


I am writing a music player app using Apple Media Player and have a button to change the volume and hide the system default volume overlay. However, all methods I find are based on UIKit.

Like:

let volumeView = MPVolumeView(frame: .zero)       
volumeView.clipsToBounds = true
volumeView.alpha = 0.00001
volumeView.showsVolumeSlider = false
view.addSubview(volumeView)

And I tried:

import Foundation
import SwiftUI
import MediaPlayer

struct VolumeView:UIViewRepresentable{
    let volumeView:MPVolumeView
    func makeUIView(context: Context) -> MPVolumeView {
        volumeView.clipsToBounds = true
        volumeView.alpha = 0.00001
        volumeView.showsVolumeSlider = false
        return volumeView
    }
    
    func updateUIView(_ uiView: MPVolumeView, context: Context) {
        
    }
    
    typealias UIViewType = MPVolumeView
}

It receives the MPVolumeView I created in my view model and I place it in a SwiftUI view. The indicator disappears but it can't change the volume.

Then I tried to make a new instance of MPVolumeView in UIViewRepresentable and it also didn't work.


Solution

  • I had the same question and your example helped me to figure it out, you were nearly there apparently.

    So here is a complete custom Volume Slider in SwiftUI

    struct VolumeSliderView: View {
    
        var primaryColor: Color
    
        @StateObject var volumeViewModel = VolumeViewModel()
        
        var body: some View {
            ZStack {
            
                VolumeView()
                    .frame(width: 0, height: 0)
            
                Slider(value: $volumeViewModel.volume, in: 0...1) {
                    Text("")
                } minimumValueLabel: {
                    Image(systemName: "speaker.fill")
                        .font(.callout)
                        .foregroundColor(primaryColor.opacity(0.6))
                } maximumValueLabel: {
                    Image(systemName: "speaker.wave.3.fill")
                        .font(.callout)
                        .foregroundColor(primaryColor.opacity(0.6))
                } onEditingChanged: { isEditing in
                    volumeViewModel.setVolume()
                }
                .tint(primaryColor.opacity(0.6))
            }
        }
    }
    
    import Foundation
    import MediaPlayer
    
    struct VolumeView: UIViewRepresentable{
        
        func makeUIView(context: Context) -> MPVolumeView {
        
           let volumeView = MPVolumeView(frame: CGRect.zero)
           volumeView.alpha = 0.001
    
           return volumeView
        }
    
        func updateUIView(_ uiView: MPVolumeView, context: Context) {
        
        }
    
    }
    

    It seems that an instance of MPVolumeView needs to be present on the screen at any time, even if it's not visible. Seems a bit hacky but works. If I only hide the view in the setVolume() function and even call volumeView.showsVolumeSlider = false there, it won't make a difference.

    class VolumeViewModel: ObservableObject {
    
        @Published var volume: Float = 0.5
    
        init() {
            setInitialVolume()
        }
    
        private func setInitialVolume() {
            volume = AVAudioSession().outputVolume
        }
    
        func setVolume() {
        
            let volumeView = MPVolumeView()
        
            let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
        
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.03)     {
                slider?.setValue(self.volume, animated: false)
            }
        }
    }
    

    Hope it helps somebody, as I just found non working examples here on SO.