אורי orihpt
אורי orihpt

Reputation: 2634

More complicated AutoLayout equations

Hi Please take a look at the following mockup:

img

I wanted to know how I can create the constraint from above:

V2.top = C1.top + n * V1.height

Because this is not something like the default equation for constraints:

item1.attribute1 = multiplier × item2.attribute2 + constant

I know I can just use AutoResizingMask but it will create a real mess in my code because my code is very complicated, and I also don't like AutoResizingMask that much.

(by the way, please answer in Swift only!)

Thank you

Upvotes: 0

Views: 51

Answers (2)

DonMag
DonMag

Reputation: 77462

You can do this with a UILayoutGuide -- from Apple's docs:

The UILayoutGuide class is designed to perform all the tasks previously performed by dummy views, but to do it in a safer, more efficient manner.

To get your desired layout, we can:

  • add a layout guide to C1
  • constrain its Top to C1 Top
  • constrain its Height to V1 Height with a "n" multiplier
  • constrain V2 Top to the guide's Bottom

Here is a complete example to demonstrate:

class GuideViewController: UIViewController {
    
    // a label on each side so we can
    //  "tap to change" v1 Height and "n" multiplier
    let labelN = UILabel()
    let labelH = UILabel()
    
    let containerView = UIView()
    let v1 = UILabel()
    let v2 = UILabel()

    // a layout guide for v2's Top spacing
    let layG = UILayoutGuide()

    // we'll change these on taps
    var n:CGFloat = 0
    var v1H: CGFloat = 30

    // constraints we'll want to modify when "n" or "v1H" change
    var v1HeightConstraint: NSLayoutConstraint!
    var layGHeightConstraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        
        v1.text = "V1"
        v2.text = "V2"
        v1.textAlignment = .center
        v2.textAlignment = .center

        containerView.backgroundColor = .systemTeal
        v1.backgroundColor = .green
        v2.backgroundColor = .yellow
        
        [containerView, v1, v2].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        containerView.addSubview(v1)
        containerView.addSubview(v2)
        
        view.addSubview(containerView)

        // add the layout guide to containerView
        containerView.addLayoutGuide(layG)

        // respect safe area
        let safeG = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // let's give the container 80-pts Top/Bottom and 120-pts on each side
            containerView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 80.0),
            containerView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 120.0),
            containerView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -120.0),
            containerView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -80.0),

            // v1 Leading / Trailing / Bottom 20-pts
            v1.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
            v1.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
            v1.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20.0),

            // just use v2's intrinisic height
            
            // v2 Leading / Trailing 20-pts
            v2.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
            v2.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),

            // layout Guide Top / Leading / Trailing
            layG.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
            layG.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0),
            layG.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0),

            // and constrain v2 Top to layout Guide Bottom
            v2.topAnchor.constraint(equalTo: layG.bottomAnchor, constant: 0.0),
            
        ])

        // layout Guide Height equals v1 Height x n
        layGHeightConstraint = layG.heightAnchor.constraint(equalTo: v1.heightAnchor, multiplier: n)
        layGHeightConstraint.isActive = true
        
        // v1 Height
        v1HeightConstraint = v1.heightAnchor.constraint(equalToConstant: v1H)
        v1HeightConstraint.isActive = true

        // "tap to change" labels
        [labelN, labelH].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            $0.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            $0.textAlignment = .center
            $0.numberOfLines = 0
            view.addSubview($0)
            let t = UITapGestureRecognizer(target: self, action: #selector(tapHandler(_:)))
            $0.addGestureRecognizer(t)
            $0.isUserInteractionEnabled = true
        }
        NSLayoutConstraint.activate([
            labelN.topAnchor.constraint(equalTo: containerView.topAnchor),
            labelN.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 8.0),
            labelN.trailingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: -8.0),
            labelN.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),

            labelH.topAnchor.constraint(equalTo: containerView.topAnchor),
            labelH.leadingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 8.0),
            labelH.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -8.0),
            labelH.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
        ])

        updateInfo()
    }

    @objc func tapHandler(_ gr: UITapGestureRecognizer) -> Void {
        guard let v = gr.view else {
            return
        }
        
        // if we tapped on the "cylcle N" label
        if v == labelN {
            
            n += 1
            if n == 6 {
                n = 0
            }
            
            // can't change multiplier directly, so
            //  de-Activate / set it / Activate
            layGHeightConstraint.isActive = false
            layGHeightConstraint = layG.heightAnchor.constraint(equalTo: v1.heightAnchor, multiplier: n)
            layGHeightConstraint.isActive = true

        }
        
        // if we tapped on the "cylcle v1H" label
        if v == labelH {
            
            v1H += 5
            if v1H > 50 {
                v1H = 30
            }

            v1HeightConstraint.constant = v1H

        }

        updateInfo()
    }
    
    func updateInfo() -> Void {
        var s: String = ""
        
        s = "Tap to cycle \"n\" from Zero to 5\n\nn = \(n)"
        labelN.text = s
        
        s = "Tap to cycle \"v1H\" from 30 to 50\n\nv1H = \(v1H)"
        labelH.text = s
        
    }
}

When you run it, it will look like this:

enter image description here

Each time you tap the left side, it will cycle the n multiplier variable from Zero to 5, and update the constraints.

Each time you tap the right side, it will cycle the v1H height variable from 30 to 50, and update the constraints.

Upvotes: 1

LGP
LGP

Reputation: 4333

It can be solved by using a helper view. A helper view is in this case just a UIView used for sizing purpose, without visible content of its own. Either set its alpha = 0 or hidden = true.

  • Set helperView.top = c1.top
  • Set helperView.height = v1.height
  • Set v2.top = helperView.bottom + 5

You also need to set the width and leading for the helper view, but their values are not important.

Upvotes: 1

Related Questions