Francesco Leoni
Francesco Leoni

Reputation: 207

UIView not updating correctly


The purple bar is "usedBar" and the gray one is "unusedBar"
I have this strange issue where I want to update the UIView width but it only updates when the new width value if smaller, let's say UIView.width = 100. If new width = 80 the view updates, if it's 120 the view doesn't update.
The new value is fetched from core data.
No storyboard involved

P.s. I noticed that Xcode creates two constraints with the previous value and the new value at the same time and it breaks the one with the new value. But I can't understand why it creates two constraints.

<NSLayoutConstraint:0x280ef28a0 usedBar.right == unusedBar.right - 147.5   (active, names: usedBar:0x1029110c0, unusedBar:0x102911220 )>
<NSLayoutConstraint:0x280ef7570 usedBar.right == unusedBar.right - 73.75   (active, names: usedBar:0x1029110c0, unusedBar:0x102911220 )>

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x280ef7570 usedBar.right == unusedBar.right - 73.75   (active, names: usedBar:0x1029110c0, unusedBar:0x102911220 )>

class ViewController: UIViewController {
    var width: CGFloat?
    let usedBar = UIView()

    var captionCount: CGFloat = 0
    var captionUsed: CGFloat = 0
    var unused: CGFloat = 0
    var captionsUsedNumber: [Bool] = [false]

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        fetchCaptions()
        width = ((scrollView.frame.width - 80) / captionCount) * unused
        configureNumbers()
    }

    func configureNumbers() {
        captionsView.addSubview(usedBar)
        usedBar.backgroundColor = .bluePurple()
        usedBar.anchors(top: captionTitleLabel.bottomAnchor, left: captionsView.leftAnchor, paddingTop: 12, paddingLeft: 20, width: width, height: 20)
    }

    func fetchCaptions() {        
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Caption")
        request.returnsObjectsAsFaults = false
        do {
            let result = try context.fetch(request)
            if result.count > 0 {
                captionCount = CGFloat(result.count)
                captionUsed = CGFloat(captionsUsedNumber.count)
            }
        }
    }
}

.anchors()

func anchors(top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, 
right: NSLayoutXAxisAnchor? = nil, paddingTop: CGFloat? = 0, paddingLeft: CGFloat? = 0, paddingBottom: CGFloat? = 0, 
paddingRight: CGFloat? = 0, width: CGFloat? = nil, height: CGFloat? = nil) {

        translatesAutoresizingMaskIntoConstraints = false

        if let top = top {
            topAnchor.constraint(equalTo: top, constant: paddingTop!).isActive = true
        }

        if let left = left {
            leftAnchor.constraint(equalTo: left, constant: paddingLeft!).isActive = true
        }

        if let bottom = bottom {
            bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom!).isActive = true
        }

        if let right = right {
            rightAnchor.constraint(equalTo: right, constant: -paddingRight!).isActive = true
        }

        if let width = width {
            widthAnchor.constraint(equalToConstant: width).isActive = true
        }

        if let height = height {
            heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }


  [1]: https://i.sstatic.net/kSV8j.jpg
  [2]: https://i.sstatic.net/Nqzp6.jpg

Upvotes: 2

Views: 1315

Answers (3)

DonMag
DonMag

Reputation: 77477

You have a slight misunderstanding of how constraints work.

When you do something like this (in your func):

widthAnchor.constraint(equalToConstant: width).isActive = true

You are not setting THE width constraint. Instead, you are adding a width constraint. If you call that func again, you are adding another width constraint. You're also adding additional top, left and height constraints, but since you are not changing those values, you don't get any conflicts... but you still get extras.

While your "helper" func can save some repetitive code, right now it's causing issues.

You could edit the func to first remove any existing constraints, then add new ones... but that can lead to other issues (such as constraints which are relied on by other views).

In your specific case, since the only constraint you want to change is the width constraint, a better approach would be to create the constraint as a class property (var), and update its .constant property when you want it to change.

Upvotes: 1

user7014451
user7014451

Reputation:

There may be other ways to do this, but for me the easiest is to create two arrays and toggle between them. And yes, I'm saying do not use setActive when setting this up.

Let's say you want a view's width to change, and that normally you'd have this:

myView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor).isActive = true
myView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor).isActive = true
myView.widthAnchor.constraint(equalToConstant: 150).isActive = true
myView.heightAnchor.constraint(equalToConstant: 40).isActive = true

But now you wish to have the width go from 150 to 300. Create two arrays, move the width constraint into it, and activate and/or deactivate accordingly:

// declare the arrays

var growTheWidth = [NSLayoutConstraint]()
var shrinkTheWidth = [NSLayoutConstraint]()


// this can be done in viewDidLoad, just make sure the view exists in the hierarchy

growTheWidth.append(myView.widthAnchor.constraint(equalToConstant: 300))
shrinkTheWidth.append(myView.widthAnchor.constraint(equalToConstant: 150))
NSLayoutConstraint.activate(shrinkTheWidth)

// these two functions will change the width and animate

func growWidth() {
    NSLayoutConstraint.deactivate(shrinkTheWidth)
    NSLayoutConstraint.activate(growTheWidth)
    UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }
}
func shrinkWidth() {
    NSLayoutConstraint.deactivate(growTheWidth)
    NSLayoutConstraint.activate(shrinkTheWidth)
    UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }
}

Notes:

  • Sequence matters. If you activate before deactivating, the layout engine will see conflicts and act in unexpected ways.
  • Remember to remove the isActive constraint I have in the original set of constraints.

Upvotes: 1

Alvaro Royo
Alvaro Royo

Reputation: 156

If you are using XIB or StoryBoard with Autolayout you will update the constraint not the frame.

You can bind the autolayout to your code like this: enter image description here

Remember delete the weak prefix or the value of your constraint will be always nil.

@IBOutlet var viewWidth: NSLayoutConstraint

Now you can edit your view with constraint like this:

self.viewWidth.constant = 120

I hope I've helped!

Upvotes: 0

Related Questions