Search code examples
iosswiftnslayoutconstraint

Setting width constraint to a UIView with a function call doesn't work anymore. iOS ( Xcode 12.0.1 )


I had the following code in Swift to fill a status bar within its container, in relation to the completion of a quiz percentage by changing its width dynamically and it worked fine in 2018:

func updateUI() {
    questionCounter.text = "\(Texts.questionCounter) \(questionNumber + 1)"
    progressBar.frame.size.width = (containerOfBar.frame.size.width / CGFloat(allQuestions.list.count)) * CGFloat(questionNumber)
}

The instantiation of the elements have been made by closures in this way:

private let containerOfBar: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = .white
    view.layer.cornerRadius = 8
    view.layer.borderColor = UIColor.white.cgColor
    view.layer.borderWidth = 2
    
    return view
}()

private let progressBar: UIView = {
    let bar = UIView()
    bar.backgroundColor = .blue
    bar.translatesAutoresizingMaskIntoConstraints = false
    
    return bar
}()

The auto-layout graphic constraints for the container and the bar, have been set in the following code only without a storyboard.

The bar itself:

progressBar.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor, constant: 2),
progressBar.topAnchor.constraint(equalTo: containerOfBar.topAnchor, constant: 2),
progressBar.bottomAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: 2),

The container of the bar:

containerOfBar.centerXAnchor.constraint(equalTo: optionsViewContainer.centerXAnchor),
containerOfBar.topAnchor.constraint(equalTo: optionsView[enter image description here][1].bottomAnchor, constant: self.view.frame.size.height/42),
containerOfBar.bottomAnchor.constraint(equalTo: optionsViewContainer.bottomAnchor, constant: -self.view.frame.size.height/42), 
containerOfBar.widthAnchor.constraint(equalTo: optionsViewContainer.widthAnchor, multiplier: 0.3),

In the link, there is the image of the completion bar drawn by code. Can't understand why the frame.width property doesn't work anymore, maybe a change in constraints workflow logic that I am missing... I tried also to use the code of the function separately, but it seems like frame.width is not dynamically usable anymore.

Any suggestions?

Progress bar


Solution

  • You are mixing constraints with explicit frame settings, which won't give you the desired results. Each time auto-layout updates the screen, it will reset your progressBar.frame.size.width back to its constraint value -- in this case, it will be Zero because you didn't give it one.

    A better approach is to set a Width Anchor on the progressBar. Make it equal to the Width Anchor of containerOfBar, with a multiplier of the percent of progress, and a constant of -4 (so you have 2-pts on each side).

    Here's an example. It uses a questionCounter of 10 ... each time you tap the screen, it will increment the "current question number" and update the progress bar:

    class ProgViewController: UIViewController {
        
        private let containerOfBar: UIView = {
            let view = UIView()
            view.translatesAutoresizingMaskIntoConstraints = false
            view.backgroundColor = .white
            view.layer.cornerRadius = 8
            view.layer.borderColor = UIColor.white.cgColor
            view.layer.borderWidth = 2
            
            return view
        }()
        
        
        private let progressBar: UIView = {
            let bar = UIView()
            bar.backgroundColor = .blue
            bar.translatesAutoresizingMaskIntoConstraints = false
            
            return bar
        }()
    
        private let questionCounter: UILabel = {
            let v = UILabel()
            v.backgroundColor = .cyan
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        var numberOfQuestions = 10
        var questionNumber = 0
        
        // width constraint of progressBar
        var progressBarWidthConstraint: NSLayoutConstraint!
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.backgroundColor = .systemYellow
            
            containerOfBar.addSubview(progressBar)
            view.addSubview(containerOfBar)
            view.addSubview(questionCounter)
    
            // create width constraint of progressBar
            //  start at 0% (multiplier: 0)
            //  this will be changed by updateUI()
            progressBarWidthConstraint = progressBar.widthAnchor.constraint(equalTo: containerOfBar.widthAnchor, multiplier: 0, constant: -4)
            progressBarWidthConstraint.priority = .defaultHigh
            
            NSLayoutConstraint.activate([
                
                progressBarWidthConstraint,
                
                progressBar.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor, constant: 2),
                progressBar.topAnchor.constraint(equalTo: containerOfBar.topAnchor, constant: 2),
                progressBar.bottomAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: -2),
                
                //The container of the bar:
                
                containerOfBar.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                containerOfBar.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
                containerOfBar.heightAnchor.constraint(equalToConstant: 50),
                containerOfBar.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
            
                // label under the container
                questionCounter.topAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: 8.0),
                questionCounter.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor),
                questionCounter.trailingAnchor.constraint(equalTo: containerOfBar.trailingAnchor),
                
            ])
    
            // every time we tap on the screen, we'll increment the question number
            let tap = UITapGestureRecognizer(target: self, action: #selector(self.nextQuestion(_:)))
            view.addGestureRecognizer(tap)
            
            updateUI()
        }
        
        @objc func nextQuestion(_ g: UITapGestureRecognizer) -> Void {
            // increment the question number
            questionNumber += 1
            // don't exceed number of questions
            questionNumber = min(numberOfQuestions - 1, questionNumber)
            updateUI()
        }
        
        func updateUI() {
            
            questionCounter.text = "Question: \(questionNumber + 1) of \(numberOfQuestions) total questions."
            
            // get percent completion
            //  for example, if we're on question 4 of 10,
            //  percent will be 0.4
            let percent: CGFloat = CGFloat(questionNumber + 1) / CGFloat(numberOfQuestions)
            
            // we can't change the multiplier directly, so
            // deactivate the width constraint
            progressBarWidthConstraint.isActive = false
            
            // re-create it with current percentage of width
            progressBarWidthConstraint = progressBar.widthAnchor.constraint(equalTo: containerOfBar.widthAnchor, multiplier: percent, constant: -4)
            
            // activate it
            progressBarWidthConstraint.isActive = true
            
            // don't mix frame settings with auto-layout constraints
            //progressBar.frame.size.width = (containerOfBar.frame.size.width / CGFloat(allQuestions.list.count)) * CGFloat(questionNumber)
            
        }
        
    }
    

    It will look like this:

    enter image description here

    enter image description here