1

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:

Without progressTint

Screenshot with progressTint calls in effect:

With ProgressTint

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.

Psiloc
  • 257
  • 3
  • 9
  • Is `ProgressView` a `UIProgressView`? Or some custom view? – DonMag Nov 09 '20 at 17:22
  • It's a standard UIProgressView. – Psiloc Nov 10 '20 at 10:42
  • Setting the tint color cannot change the progress bar length. There is some confusion here... you say *"green/amber/red"* but your first image shows blue? And, from the code you have shown, we have no way of knowing what values you are getting. Use debug breakpoints (or some print statements) to inspect the values of `Max` and `Progress`. – DonMag Nov 10 '20 at 14:45
  • The blue version of the screenshot is where I haven't set the progressTint programmatically. The green/amber/red is the same screenshot where I HAVE set the tint in code. As you can see, the green item has progressed further. All I have changed between the two screenshots is those three Cell.ProgressView.progressTintColor calls are either commented out or not. – Psiloc Nov 10 '20 at 15:04
  • I've clarified the question and added the values as comments. – Psiloc Nov 10 '20 at 15:24
  • ***Something*** else is going on with your code... maybe somewhere you haven't shown. And, based on your previous question (here)[https://stackoverflow.com/questions/63689066/progressview-inside-tableviewcell-gets-weird-rounded-corners-on-scroll] you are doing ***way too much*** in your `cellForRowAt` - most of that should be in your cell's init. Also, you should be using `leadingLowerCase` for variable names (use `LeadingUpperCase` for class names). Try using some explicit values for your `Max` and `Progress` and see what you get. – DonMag Nov 10 '20 at 17:20
  • By the way -- are you creating your cell via code? or with a Storyboard Prototype? – DonMag Nov 10 '20 at 17:21
  • Storyboard prototype. I've had a hunch I'm doing too much in the cellForRowAt, I'll try to switch some stuff to the cell init. There's still going to be a lot in there though but this is to the spec of the app. – Psiloc Nov 11 '20 at 14:04
  • I've already tried hardcoding the values. I completely understand where you're coming from but I assure you the values are fine. Thanks for your help so far – Psiloc Nov 11 '20 at 14:07

1 Answers1

1

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:

  • backgroundColor = white
  • tintColor = blue
  • cornerRadius = 4
  • intrinsic height = 4

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)
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Thanks for your answer. Where can I find this SimpleProgressView? – Psiloc Nov 12 '20 at 09:58
  • @Psiloc - it's the first code block in my answer. In your Storyboard Prototype cell, replace the `UIProgressView` with a plain `UIView`, then assign its Custom Class to `SimpleProgressView` and connect it via `@IBOutlet`. – DonMag Nov 12 '20 at 14:33
  • I don't have a class called SimpleProgressView and I can't find anything about it on Google – Psiloc Nov 13 '20 at 09:08
  • Ah, I do apologise, it had been a long day! I'll give this a go as soon as I can. – Psiloc Nov 16 '20 at 09:20
  • This worked! However, I must admit I'm completely unsure what part of it resolved the issue. – Psiloc Nov 17 '20 at 11:07
  • @Psiloc - the problem was that the `UIProgressView` apparently has a bug when changing the height and setting the tint color. What resolved the issue was using a fairly simple custom `UIView` that mimics the functionality of a `UIProgressView`. – DonMag Nov 17 '20 at 13:01