iosxcodempvolumeview

How to use MPVolume View correctly?


After implementing DonMag's answer, work's great After implementing DonMag's answer, work's great

-This was my Question: This is an audio player, I removed all the other lines for you to be easy in reading. The problem is in MPVolumeView. When user swipes all the way to maximum the button of the slider hovers over the connectivity button. When user swipes the button of the slider all the way to minimum the button of the slider doesn't move to the end.

-Dear DonMag, I am really thankful to you, It works! and HOW! I am adding screenshots. I believe your answer will be helpful to a lot of self tights.

When user swipes the button of the slider all the way to minimum the button of the slider doesn't move to the end.

When user swipes all the way to maximum the button of the slider hovers over the connectivity button.

import UIKit
import AVFoundation
import MediaPlayer
import AVKit

class AudioPlayerViewControllerQ1: UIViewController {
      
   
@IBOutlet var holder: UIView!


    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
                
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        
        if holder.subviews.count == 0 {
        }
        
        let volumeView = MPVolumeView(frame: CGRect(x: 20,
                                                    y: holder.frame.size.height - 80,
                                                    width: holder.frame.size.width-40,
                                                    height: 30))
        
        holder.addSubview(volumeView)
        
    }

    private func setupView() {
        setupConstraints()
    }
    
    private func setupConstraints() {
        NSLayoutConstraint.activate([
            holder.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            holder.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            holder.topAnchor.constraint(equalTo: view.topAnchor),
            holder.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        UIApplication.shared.isIdleTimerDisabled = true
    }
        
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        UIApplication.shared.isIdleTimerDisabled = false
        
    }

    
}

Solution

  • After quick research and experimentation -- it appears MPVolumeView is rather buggy :(

    When instantiated, if the current device volume is greater than 0, the thumb will be offset on the x-axis. The higher the volume, the larger the offset.

    Also, it doesn't play well at all with auto-layout constraints.

    We can get around this by subclassing MPVolumeView and "fixing" the slider rect:

    class MyVolumeView: MPVolumeView {
        override func volumeSliderRect(forBounds bounds: CGRect) -> CGRect {
            // this will avoid the thumb x-offset issue
            //  while keeping the route button vertically aligned
            return bounds.insetBy(dx: 12.0, dy: 0.0).offsetBy(dx: -12.0, dy: -5.0)
        }
    }
    

    Then, to correct the problems with the vertical layout, we will want to offset the Y position when we set its frame.

    Here's a quick example of one way to do that. I've embedded MyVolumeView in a "container" view, and used a property observer to update the frame whenever the container view's bounds changes:

    class AudioPlayerViewControllerQ1: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // let's give the view a background color so we can easily see its frame
            view.backgroundColor = .systemYellow
            
            // assuming "holder" view has buttons and other controls
            //  along with the MyVolumeView near the bottom
            let holder = UIView()
            holder.backgroundColor = .darkGray
            holder.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(holder)
            
            // create a separate "container" view for the MyVolumeView
            let volumeViewContainer = UIView()
            // we'll make it red for now so we can see it
            volumeViewContainer.backgroundColor = .red
            volumeViewContainer.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(volumeViewContainer)
            
            // respect safe-area
            let g = view.safeAreaLayoutGuide
    
            NSLayoutConstraint.activate([
                
                // let's make the holder 20-points inset on leading/trailing
                holder.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                holder.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                // holder height (for this example) is 240.0
                holder.heightAnchor.constraint(equalToConstant: 240.0),
                // let's put its bottom 60-points from the bottom (of the safe area)
                holder.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -160.0),
    
                // volume view container leading/trailing equal to holder
                volumeViewContainer.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 0.0),
                volumeViewContainer.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: 0.0),
                // volume view container bottom equal to holder bottom
                volumeViewContainer.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: 0.0),
                // volume view container height equal to 30-points
                volumeViewContainer.heightAnchor.constraint(equalToConstant: 30.0),
    
            ])
    
            // now we'll add a MPVolumeView to the container
            let v = MyVolumeView()
            volumeViewContainer.addSubview(v)
    
            // we'll use a property observer to update the MyVolumeView frame
            //  whenever the container bounds changes
            volumeViewContainer.addObserver(self, forKeyPath: "bounds", context: nil)
    
        }
    
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            if keyPath == "bounds" {
                // make sure we're getting notified of the MyVolumeView container view
                if let cv = object as? UIView,
                   let mpv = cv.subviews.first as? MyVolumeView {
                    // set MyVolumeView frame to container view's bounds
                    // and offset its y-position by 4-points (because of its buggy layout)
                    mpv.frame = cv.bounds.offsetBy(dx: 0.0, dy: 4.0)
                }
            }
        }
    
    }
    

    It looks like this when running:

    enter image description here

    and we can drag the thumb all the way to the left:

    enter image description here

    and to the right (without overlapping the route button):

    enter image description here


    Edit

    Here are a couple simplified examples...

    Using CGRect frames instead of constraints (as requested by the OP):

    class AudioPlayerViewControllerQ1: UIViewController {
    
        let holder = UIView()
        let myVolumeView = MyVolumeView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // let's give the view a background color so we can easily see its frame
            view.backgroundColor = .systemYellow
            
            // assuming "holder" view has buttons and other controls
            //  along with the MPVolumeView near the bottom
            holder.backgroundColor = .darkGray
            view.addSubview(holder)
            
            // now we'll add a MPVolumeView to the container
            holder.addSubview(myVolumeView)
    
        }
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
    
            // let's make sure this only executes if the holder view frame has not been set yet
            if holder.frame.width != 320.0 {
                // set holder view frame to 320 x 240
                holder.frame = CGRect(x: 0, y: 0, width: 320.0, height: 240.0)
                // center it in the view
                holder.center = view.center
                // set myVolumeView frame to same width as holder view
                //  30-points height, at bottom of holder view
                myVolumeView.frame = CGRect(x: 0.0, y: holder.frame.height - 30.0, width: holder.frame.width, height: 30.0)
            }
            
        }
    
    }
    

    and, this one using constraints:

    class AudioPlayerViewControllerQ1: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // let's give the view a background color so we can easily see its frame
            view.backgroundColor = .systemYellow
            
            // assuming "holder" view has buttons and other controls
            //  along with the MPVolumeView near the bottom
            let holder = UIView()
            holder.backgroundColor = .darkGray
            holder.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(holder)
            
            // now we'll add a MPVolumeView to the container
            let myVolumeView = MyVolumeView()
            myVolumeView.translatesAutoresizingMaskIntoConstraints = false
            holder.addSubview(myVolumeView)
            
            // respect safe-area
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // set holder view frame to 320 x 240
                holder.widthAnchor.constraint(equalToConstant: 320.0),
                holder.heightAnchor.constraint(equalToConstant: 240.0),
                // center it
                holder.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                holder.centerYAnchor.constraint(equalTo: g.centerYAnchor),
                
                // constrain myVolumeView leading/trailing/bottom equal to holder view
                myVolumeView.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 0.0),
                myVolumeView.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: 0.0),
                myVolumeView.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: 0.0),
                // myVolumeView height
                myVolumeView.heightAnchor.constraint(equalToConstant: 30.0),
    
            ])
            
        }
        
    }