Search code examples
swiftautolayoutuikitnslayoutconstraint

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


Solution

  • 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.