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.
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:
It looks like this when running:
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 :)