swiftuitableviewuiprogressviewtintcolor

Programmatically setting progressTint on ProgressView changes progress bar size


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.


Solution

  • 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)
    }