iosswiftuiprogressview

UIProgressView reveal progressImage image


I have a UIProgressView and I'm giving it a progressImage that I'd like to reveal as the progress bar progresses. How can I do this?

What currently happens is the whole images always fits inside the progress bar, it just gets compressed. I'd like to, instead, just show a partial (e.g. the left side) of the image.


Solution

  • You'll need to create your own "custom progress view."

    One approach is to use a CALayer as a mask on the image view, adjusting the size of the layer to be a percentage of the width of the custom view.

    Here's a quick example...

    Custom Progress View

    class MyProgressView: UIView {
        // image that will be "revealed"
        public var image: UIImage? {
            didSet {
                bkgView.image = image
            }
        }
        public var progress: Float {
            set {
                // keep the value between 0.0 and 1.0
                _progress = max(min(newValue, 1.0), 0.0)
                setNeedsLayout()
            }
            get {
                return _progress
            }
        }
        private var _progress: Float = 0.0
        
        private let bkgView = UIImageView()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            bkgView.translatesAutoresizingMaskIntoConstraints = false
            addSubview(bkgView)
            let g = self
            NSLayoutConstraint.activate([
                bkgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                bkgView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                bkgView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                bkgView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            // width to "reveal" will be the percentage of self's width
            let w: CGFloat = bounds.width * CGFloat(_progress)
            var r = bounds
            r.size.width = w
            
            // create a mask layer
            let msk = CALayer()
            // can be any color other than clear
            msk.backgroundColor = UIColor.black.cgColor
            msk.frame = r
            bkgView.layer.mask = msk
        }
    
    }
    

    Sample view controller

    class MyProgessVC: UIViewController {
    
        let myProgressView = MyProgressView()
        let standardProgressView = UIProgressView()
        let infoLabel = UILabel()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let g = view.safeAreaLayoutGuide
    
            // make sure we can load the image we want to use for our custom progress view
            guard let img = UIImage(named: "pvBKG") else { return }
            
            // add the standard progress view
            standardProgressView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(standardProgressView)
    
            // add our custom progress view
            myProgressView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(myProgressView)
    
            // add a slider to set the progress view percentage
            let slider = UISlider()
            slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
            
            slider.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(slider)
            
            // add a label to show the current progress
            infoLabel.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(infoLabel)
            infoLabel.textAlignment = .center
            
            NSLayoutConstraint.activate([
    
                // let's put the standard progress view near the top
                standardProgressView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                // with 20-points on each side
                standardProgressView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                standardProgressView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
                // put our custom "progress view" below the standard one
                myProgressView.topAnchor.constraint(equalTo: standardProgressView.bottomAnchor, constant: 40.0),
                // with 20-points on each side
                myProgressView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                myProgressView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                // we'll make the height equal to the image height
                myProgressView.heightAnchor.constraint(equalToConstant: img.size.height),
    
                // put the slider below the progress views
                slider.topAnchor.constraint(equalTo: myProgressView.bottomAnchor, constant: 40.0),
                slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
                // put the info label below the slider
                infoLabel.topAnchor.constraint(equalTo: slider.bottomAnchor, constant: 40.0),
                infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
            ])
            
            // color for progress view "right-side"
            myProgressView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            
            // set the "reveal" image
            myProgressView.image = img
            
            updateInfoLabel()
        }
        @objc func sliderChanged(_ sender: UISlider) {
            // set .progress on each to the slider value
            myProgressView.progress = sender.value
            standardProgressView.progress = sender.value
            updateInfoLabel()
        }
        func updateInfoLabel() {
            infoLabel.text = "\(myProgressView.progress)"
        }
    
    }
    

    We add a "standard" UIProgressView, an instance of our custom MyProgressView, a UISlider to interactively set the progress value, and a label to show the value.

    Using this image for the progress view "reveal" image:

    enter image description here

    It looks like this when running:

    enter image description here enter image description here

    enter image description here enter image description here

    If you want to emulate the animation capability of the default UIProgressView (as in calling .setProgress(0.75, animated: true)) you'll have a little more work to do :)