0
votes

I have a cake of 3 UIView layers with programmatically constraints.

Constructed function for programmatically set up constraints:

func setupViewConstraints(item:UIView, leadingTo:NSLayoutXAxisAnchor, leadingCon:CGFloat, 
trailingTo:NSLayoutXAxisAnchor, trailingCon:CGFloat, topTo:NSLayoutYAxisAnchor, 
topCon:CGFloat, bottomTo:NSLayoutYAxisAnchor, bottomCon:CGFloat) {
    item.translatesAutoresizingMaskIntoConstraints = false
    item.leadingAnchor.constraint(equalTo: leadingTo, constant: leadingCon).isActive = true
    item.trailingAnchor.constraint(equalTo: trailingTo, constant: trailingCon).isActive = true
    item.topAnchor.constraint(equalTo: topTo, constant:topCon).isActive = true
    item.bottomAnchor.constraint(equalTo: bottomTo, constant:bottomCon).isActive = true
}

The lowest base layer is lightGray.

view = UIView()
view.backgroundColor = .lightGray

The 2nd layer contains 2 UIView (red and blue) with constraints .

let red = UIView()
red.backgroundColor = .red
view.addSubview(red)
setupViewConstraints(item: red, leadingTo: view.leadingAnchor, leadingCon: 0, trailingTo: view.trailingAnchor, trailingCon: -(view.frame.width)*0.2), topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: -(view.frame.width)*0.8)

let blue = UIView()
blue.backgroundColor = .blue
view.addSubview(blue)
setupViewConstraints(item: blue, leadingTo: view.leadingAnchor, leadingCon: 0, trailingTo: view.trailingAnchor, trailingCon: -(view.frame.width)*0.2), topTo: red.bottomAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)

3 UIView

And on top i have yellow UIView layer, which overlaps all the lower layers.

let yellow = UIView()
yellow.backgroundColor = .yellow
view.addSubview(yellow)
setupViewConstraints(item: yellow, leadingTo: view.leadingAnchor, leadingCon: 0, trailingTo: view.trailingAnchor, trailingCon: 0, topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)

Also, i have UINavigationBar with UINavigationItem inside the yellow UIView.

//Add navigation item and buttons
        naviItem = UINavigationItem()
        naviItem.setRightBarButton(UIBarButtonItem(barButtonSystemItem:.add, target:self, action:#selector(goToDestVC)), animated: true)
        naviItem.setLeftBarButton(UIBarButtonItem(image: UIImage(named: "hamburger_slim_30"), style: .plain, target: self, action: #selector(hamburgerBtnPressed)), animated: true)

        //Add navigation bar with transparent background
        naviBar = UINavigationBar()
        naviBar.setBackgroundImage(UIImage(), for: .default)
        naviBar.shadowImage = UIImage()
        naviBar.isTranslucent = true

// Assign the navigation item to the navigation bar
        naviBar.items = [naviItem]

        view.addSubview(naviBar)
        setupViewConstraints(item: naviBar, leadingTo: yellow.leadingAnchor, leadingCon: 0, trailingTo: yellow.trailingAnchor, trailingCon: 0, topTo: yellow.topAnchor, topCon: 0, bottomTo: yellow.bottomAnchor, bottomCon: -(view.frame.height)*0.9))

Top UIView layer with UINavigationBar

And i have hamburgerBtnPressed function, which should shift the yellow layer to the right by 80% (I change the values of leading and trailing constants by 80%), but this does not work!!!

var hamburgerMenuIsVisible = false

    @objc func hamburgerBtnPressed(_ sender: Any) {

        if !hamburgerMenuIsVisible {

            let menuWidth = (self.view.frame.width)*0.8

              setupViewConstraints(item: layoutView, leadingTo: view.leadingAnchor, leadingCon: menuWidth, trailingTo: view.trailingAnchor, trailingCon: menuWidth, topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)

            hamburgerMenuIsVisible = true

        } else {

            setupViewConstraints(item: layoutView, leadingTo: view.leadingAnchor, leadingCon: 0, trailingTo: view.trailingAnchor, trailingCon: 0, topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)

            hamburgerMenuIsVisible = false
        }

        // layoutIfNeeded() lays out the subviews immediately and forces the layout before drawing
        UIView.animate(withDuration: 0.2, delay:0.0, options: .curveEaseIn, animations: {

            self.view.layoutIfNeeded()

        }) { (animationComplete) in
            print("Animation is complete!")
        }
    }

But if I change the values of leading and trailing constants to negative, everything will work, and the menu will shift to the left without any problems.

let menuWidth = -(self.view.frame.width)*0.8

To the left

Please explain.. what's the issue? Why the yellow UIView shifted to the left with negative values of constraints, and does not work with positive values of constraints? and gives an error:

Probably at least one of the constraints in the following list is one you don't want. 
(
    "<NSLayoutConstraint:0x6040002853c0 UIView:0x7fa947c35850.trailing == UIView:0x7fa947e1d2d0.trailing   (active)>",
    "<NSLayoutConstraint:0x604000092750 UIView:0x7fa947c35850.trailing == UIView:0x7fa947e1d2d0.trailing + 331.2   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x604000092750 UIView:0x7fa947c35850.trailing == UIView:0x7fa947e1d2d0.trailing + 331.2   (active)>

Update: I have choose Option 2: Keep a reference to the constraint that you want to change and just adjust its constant. Need to call setNeedsLayout too before layoutIfNeeded.

Updated code:

var leadingC: NSLayoutConstraint!
var trailingC: NSLayoutConstraint!
var yellow: UIView!

loadView():

    yellow = UIView()
        yellow.backgroundColor = .yellow
        view.addSubview(yellow)

        //Set up leading and trailing constraints for handling yellow view shift
        leadingC = yellow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0)
        trailingC = yellow.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0)

//Put leadingC.constant and trailingC.constant into the function
        setupViewConstraints(item: yellow, leadingTo: view.leadingAnchor, leadingCon: leadingC!.constant, trailingTo: view.trailingAnchor, trailingCon: trailingC.constant, topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)

Updated Hamburger function:

 @objc func hamburgerBtnPressed(_ sender: Any) {

        if !hamburgerMenuIsVisible {

            let menuWidth = (self.view.frame.width)*0.8


            leadingC!.constant = menuWidth
            trailingC!.constant = menuWidth

            print(leadingC.constant, trailingC.constant)

            hamburgerMenuIsVisible = true

        } else {

            leadingC!.constant = 0
            trailingC!.constant = 0


            hamburgerMenuIsVisible = false
        }

        // layoutIfNeeded() lays out the subviews immediately and forces the layout before drawing
        UIView.animate(withDuration: 0.2, delay:0.0, options: .curveEaseIn, animations: {

            self.view.setNeedsLayout()
            self.view.layoutIfNeeded()

        }) { (animationComplete) in
            print("Animation is complete!")
        }
    }
    var hamburgerMenuIsVisible = false

I have no errors and "Animation complete!" was printed too, but nothing happens on the screen, no animation.

Output

1
This is one reason I personally do not combine constraint creation into a generic function - too hard to decipher. First, if I read you correctly, there's no problem with red, blue, and gray views, right? If so, please remove them from your question as it makes for noise. Are you trying to compute 80% of a frame in order to (a) move or (b) shrink the yellow view? Because in the former, you've already said it works - because you are moving leading/trailing anchors. But if you want the latter, you need to change the width anchor - which I'm not seeing in your generic function. - dfd
I want to shift yellow UIView to the right for 80%, in order to (a) move yellow view. I'm using trailing anchor for "width", and I can set only width OR trailing anchor, otherwise going to get conflicts. - Rurom
Correct. So it sounds like you want a static width. That's part of the confusion... (1) Why are you computing 80% of a frame width? That doesn't change. More, (2) to keep this static width you alter the constants - possibly it will work with the multipliers but I think not - of the leading and trailing by a negative value to shift/move left and back to their original value to shift right. I thought that's what you said worked? What am I missing? - dfd
I'm computing 80% of frame width, because it's a width of 2nd layer menu , if the yellow view shifts to the left for 80%, it's opens hidden menu. The width can be any, the question is not about that. In this case default value of leading and trailing constant is equal to 0, i change the values by the same amount, in this case (.trailing/leading + 331.2 (80% of the view width)) and the yellow view must shift to the right. -->leading+331.2 -->trailing+331.2. With negative values it's works, and shift the yellow view to the left, but doesn't works with positive values. - Rurom

1 Answers

1
votes

Firstly, it needs negative values because the constraints need to be set up in the correct direction. Change this and you can remove all of those negative constants:

item.leadingAnchor.constraint(equalTo: leadingTo, constant: leadingCon).isActive = true
item.trailingAnchor.constraint(equalTo: trailingTo, constant: trailingCon).isActive = true
item.topAnchor.constraint(equalTo: topTo, constant:topCon).isActive = true
item.bottomAnchor.constraint(equalTo: bottomTo, constant:bottomCon).isActive = true

to

item.leadingAnchor.constraint(equalTo: leadingTo, constant: leadingCon).isActive = true
trailingTo.constraint(equalTo: item.trailingAnchor, constant: trailingCon).isActive = true
item.topAnchor.constraint(equalTo: topTo, constant:topCon).isActive = true
bottomTo.constraint(equalTo: item.bottomAnchor, constant:bottomCon).isActive = true

Secondly, every time you call setupViewConstraints you are creating and activating another set of constraints.

Option 1:

Remove all constraints for the yellow view before setting them up again.

Option 2:

Keep a reference to the constraint that you want to change and just adjust its constant. You may need to call setNeedsLayout too before layoutIfNeeded.

Option 3:

Add 2 contraints. The initial leading constraint, and one with the width you desire. Change the priority of the first constraint to 999 (default is 1000) and toggle the isActive property of the other when you want to show/hide the menu.

let leading = yellow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0)
leading.priority = UILayoutPriority(999)
leading.isActive = true

let otherConstraint = yellow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: (view.frame.width)*0.8)
otherConstraint.isActive = false // toggle this property to show/hide

Option 2 is probably going to be the best for performance. From the apple docs:

Setting the constant on an existing constraint performs much better than removing the constraint and adding a new one that's exactly like the old except that it has a different constant