It's taken me days to track down where this issue is coming from.
I have a TableView with rows of custom Table Cells, inside each of which is a progress view. The app calls for the progress view tint to be green/amber/red based on how full it is.
I have discovered that setting the progressTint programmatically causes the progress bar to appear fuller than it should do.
Relevant code (tableView cellForRowAt):
let Max:Double = MyGroup!.EndTimeSeconds - MyGroup!.StartTimeSeconds //10771
let Progress:Double = Date().timeIntervalSince1970 - MyGroup!.StartTimeSeconds //1599.7007069587708
if (Max >= Progress) {
Cell.DescriptionLabel.textColor = UIColor.black
Cell.SubtitleLabel.textColor = UIColor.black
Cell.TargetDeliveryTimeLabel.textColor = UIColor.pts_darkergrey
Cell.ProgressView.setProgress(Float(Progress / Max), animated: false)
Cell.ProgressView.progress = Float(Progress / Max)
Cell.ProgressView.progressTintColor = UIColor.pts_green //if i comment these out it works.
if (Max * 0.75 <= Progress) {
Cell.ProgressView.progressTintColor = UIColor.pts_pbamber //if i comment these out it works.
}
} else {
Cell.DescriptionLabel.textColor = UIColor.white
Cell.SubtitleLabel.textColor = UIColor.white
Cell.TargetDeliveryTimeLabel.textColor = UIColor.white
Cell.ProgressView.setProgress(1, animated: false)
Cell.ProgressView.progress = 1
Cell.ProgressView.progressTintColor = UIColor.pts_pbred //if i comment these out it works.
}
Cell.ProgressView.layer.cornerRadius = 4
Cell.ProgressView.clipsToBounds = true
Screenshot with progressTint calls commented out:
Screenshot with progressTint calls in effect:
Notice the second item's progress bar erroneously gets filled to almost 50% when the tint is set.
The progress bar should fill linearly over time - but this will stay stationary until the progress legitimately passes this point and then it continues like normal.
I may be seeing things but the problem seems to affect the top two items constantly, and not the rest (either as much, or not at all)
I have tried both ProgressView.progress and ProgressView.setProgress, and ProgressView.progressTintColor and PogressView.tintColor.
After some searching and testing... it would appear that the standard UIProgressView
does not like some combination(s) of height, tint color and/or layer modified.
Try replacing your UIProgressView
with this SimpleProgressView
It has defaults of:
You should be able to use this as a direct replacement - no need to make any other changes to your existing code. It's @IBDesignable
with cornerRadius
and progress
as @IBInspectable
so you can set those and see the result in Storyboard.
@IBDesignable
class SimpleProgressView: UIView {
@IBInspectable public var cornerRadius: CGFloat = 0 {
didSet {
progressBarView.layer.cornerRadius = cornerRadius
layer.cornerRadius = cornerRadius
}
}
private let progressBarView = UIView()
private var widthConstraint: NSLayoutConstraint!
// default height of
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: 4.0)
}
// set the background color of the progressBarView to the tint color
override var tintColor: UIColor! {
didSet {
progressBarView.backgroundColor = tintColor
}
}
// update width constraint multiplier when progress changes
@IBInspectable public var progress: Float = 0 {
didSet {
if let wc = widthConstraint {
// cannot modify multiplier directly, so
// deactivate
wc.isActive = false
// create new width constraint with percent as multiplier
// maximum of 1.0
let pct = min(progress, 1.0)
self.widthConstraint = progressBarView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: CGFloat(pct))
// activate new width constraint
self.widthConstraint.isActive = true
}
}
}
// we can set .progress property directly, or
// call setProgress (with optional animated parameter)
public func setProgress(_ p: Float, animated: Bool) -> Void {
// don't allow animation if frame height is zero
let doAnim = animated && progressBarView.frame.height != 0
self.progress = p
if doAnim {
UIView.animate(withDuration: 0.3, animations: {
self.layoutIfNeeded()
})
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
if backgroundColor == nil {
backgroundColor = UIColor.black.withAlphaComponent(0.1)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
// default background color: black with 0.1 alpha
if backgroundColor == nil {
backgroundColor = UIColor.black.withAlphaComponent(0.1)
}
// default tint color
tintColor = .blue
// default corner radius
cornerRadius = 4
progressBarView.translatesAutoresizingMaskIntoConstraints = false
addSubview(progressBarView)
// create width constraint
// progressBarView width will be set to percentage of self's width
widthConstraint = progressBarView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.0)
NSLayoutConstraint.activate([
// constrain progressBarView Top / Leading / Bottom to self
progressBarView.topAnchor.constraint(equalTo: topAnchor),
progressBarView.leadingAnchor.constraint(equalTo: leadingAnchor),
progressBarView.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate width constraint
widthConstraint,
])
clipsToBounds = true
}
}
Here's a quick test implementation, comparing UIProgressView
on top and SimpleProgressView
below. Progress bar will start at 10%, increment by 10% with each tap on the view, and change colors at 25, 75 and 100%:
class ViewController: UIViewController {
let uiProgressView = UIProgressView()
let simpleProgressView = SimpleProgressView()
let labelA = UILabel()
let labelB = UILabel()
var curProgress: Float = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
labelA.text = "Default UIProgressView"
labelB.text = "Custom SimpleProgressView"
[labelA, uiProgressView, labelB, simpleProgressView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
labelA.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
labelA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
uiProgressView.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 12.0),
uiProgressView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
uiProgressView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
uiProgressView.heightAnchor.constraint(equalToConstant: 80.0),
labelB.topAnchor.constraint(equalTo: uiProgressView.bottomAnchor, constant: 40.0),
labelB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
simpleProgressView.topAnchor.constraint(equalTo: labelB.bottomAnchor, constant: 12.0),
simpleProgressView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
simpleProgressView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
simpleProgressView.heightAnchor.constraint(equalToConstant: 80.0),
])
let t = UITapGestureRecognizer(target: self, action: #selector(self.incProgress(_:)))
view.addGestureRecognizer(t)
// start at 10%
incProgress(nil)
}
@objc func incProgress(_ g: UITapGestureRecognizer?) -> Void {
// increment progress by 10% on each tap, up to 100%
curProgress = min(1.0, curProgress + 0.10)
uiProgressView.progress = curProgress
simpleProgressView.progress = curProgress
let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.maximumFractionDigits = 2
if let sPct = formatter.string(for: curProgress) {
labelA.text = "Default UIProgressView: " + sPct
labelB.text = "Custom SimpleProgressView: " + sPct
}
print(curProgress)
if curProgress == 1.0 {
uiProgressView.tintColor = .pts_red
simpleProgressView.tintColor = .pts_red
} else if curProgress >= 0.75 {
uiProgressView.tintColor = .pts_amber
simpleProgressView.tintColor = .pts_amber
} else if curProgress >= 0.25 {
uiProgressView.tintColor = .pts_green
simpleProgressView.tintColor = .pts_green
} else {
uiProgressView.tintColor = .pts_blue
simpleProgressView.tintColor = .pts_blue
}
}
}
I tried to match your custom colors:
extension UIColor {
static let pts_green = UIColor(red: 0.35, green: 0.75, blue: 0.5, alpha: 1.0)
static let pts_amber = UIColor(red: 0.95, green: 0.7, blue: 0.0, alpha: 1.0)
static let pts_red = UIColor(red: 0.9, green: 0.35, blue: 0.35, alpha: 1.0)
static let pts_blue = UIColor(red: 0.25, green: 0.75, blue: 1.0, alpha: 1.0)
static let pts_darkergrey = UIColor(white: 0.2, alpha: 1.0)
}