Search code examples
iosuitableviewswift3visual-format-language

UITableview Header Horizontal and Vertical constraints using VSL


I have a header view with a label and a button. I'm adding the constraints like this:

//create a view, button and label
view.addSubview(label)
view.addSubview(button)

label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false

let views = ["label": label, "button": button, "view": view]

let horizontallayoutContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-19-[label]-60-[button]-22-|", options: .alignAllCenterY, metrics: nil, views: views)
view.addConstraints(horizontallayoutContraints)

let verticalLayoutContraint = NSLayoutConstraint(item: label, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0)
view.addConstraint(verticalLayoutContraint)

return view

This works really well but now I'd like to add a divider view that spans the width above the label and button. Something like this:

let frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 10)
let divView = UIView(frame: frame)
divView.backgroundColor = UIColor.lightGray

I can't seem to figure out the combination of constraints to make this happen. Basically I want the divView to span the width of the tableview and the existing views to sit below it. Ideally I could nest it like this:

V:|[divView]-20-[H:|-19-[label]-60-[button]-22-|]-20-|

Any experts out there that can help me figure this out? I could just make a NIB but I'd prefer to do it programmatically.


Solution

  • Granted, I have done very, very little with Visual Format Language, but as far as I know you cannot nest in that manner.

    Depending on exactly what you are trying to get as an end result, and what else might get added to this view (labels? images? etc), you might find it easier to use a UIStackView or two.

    However, here is an example of using VFL... this will run as-is in a Playground, so it will be easy to make adjustments to see the effect. Also note that I did it two ways - align the label and button to the divider, or align the divider to the label and button. The comments and the if block should be pretty self-explanatory. The colors are just to make it easy to see where the elements' frames end up.

    import UIKit
    import PlaygroundSupport
    
    let container = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 600))
    
    container.backgroundColor = UIColor.green
    
    PlaygroundPage.current.liveView = container
    
    // at this point, we have a 600 x 600 green square to use as a playground "canvas"
    
    // create a 400x100 view at x: 40 , y: 40 as a "header view"
    let headerView = UIView(frame: CGRect(x: 40, y: 40, width: 400, height: 100))
    headerView.backgroundColor = UIColor.blue
    
    // add the Header View to our "main container view"
    container.addSubview(headerView)
    
    
    var label: UILabel = {
        let l = UILabel()
        l.backgroundColor = UIColor.yellow
        l.text = "The Label"
        return l
    }()
    
    var button: UIButton = {
        let l = UIButton()
        l.backgroundColor = UIColor.red
        l.setTitle("The Button", for: .normal)
        return l
    }()
    
    var divView: UIView = {
        let v = UIView()
        v.backgroundColor = UIColor.lightGray
        return v
    }()
    
    
    headerView.addSubview(divView)
    headerView.addSubview(label)
    headerView.addSubview(button)
    
    divView.translatesAutoresizingMaskIntoConstraints = false
    label.translatesAutoresizingMaskIntoConstraints = false
    button.translatesAutoresizingMaskIntoConstraints = false
    
    var vcs: [NSLayoutConstraint]
    
    var views = ["divView": divView, "label": label, "button": button, "headerView": headerView]
    
    
    let bAlignToDivider = true
    
    if bAlignToDivider {
    
        // use the width of the divView to control the left/right edges of the label and button
    
        // V: pin divView to the top, with a height of 10
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "V:|[divView(10)]", options: [], metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
        // H: pin divView 20 from the left, and 20 from the right
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "H:|-20-[divView]-20-|", options: [], metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
        // V: pin label to bottom of divView (plus spacing of 8)
        //    using .alignAllLeft will pin the label's left to the divView's left
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "V:[divView]-8-[label]", options: .alignAllLeft, metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
        // V: pin button to bottom of divView (plus spacing of 8)
        //    using .alignAllRight will pin the button's right to the divView's right
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "V:[divView]-8-[button]", options: .alignAllRight, metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
        // H: add ">=0" spacing between label and button, so they use intrinsic widths
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "H:[label]-(>=0)-[button]", options: .alignAllCenterY, metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
    }
    else
    {
    
        // use left/right edges of the label and button to control the width of the divView
    
        // H: pin label 20 from left
        //    pin button 20 from right
        //    use ">=0" spacing between label and button, so they use intrinsic widths
        //    also use .alignAllCenterY to vertically align them
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "H:|-20-[label]-(>=0)-[button]-20-|", options: .alignAllCenterY, metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
        // V: pin divView to the top, with a height of 10
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "V:|[divView(10)]", options: [], metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
        // V: pin label to bottom of divView (plus spacing of 8)
        //    using .alignAllLeft will pin the divView's left to the label's left
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "V:[divView]-8-[label]", options: .alignAllLeft, metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
        // V: pin button to bottom of divView (plus spacing of 8)
        //    using .alignAllRight will pin the divView's right to the button's right
        vcs = NSLayoutConstraint.constraints(withVisualFormat:
            "V:[divView]-8-[button]", options: .alignAllRight, metrics: nil, views: views)
        headerView.addConstraints(vcs)
    
    }
    

    Edit: Here is another variation.

    This time, the "header view" will have only the x,y position set... Its width and height will be auto-determined by its content.

    The gray "div" view's position and width will be controlled by constraining it to the label and button, which will use your specified values:

    "H:|-19-[label]-60-[button]-22-|"
    

    Again, you can just copy/paste this into a playground page...

    import UIKit
    import PlaygroundSupport
    
    let container = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 600))
    
    container.backgroundColor = UIColor.green
    
    PlaygroundPage.current.liveView = container
    
    // at this point, we have a 600 x 600 green square to use as a playground "canvas"
    
    var label: UILabel = {
        let l = UILabel()
        l.backgroundColor = UIColor.yellow
        l.text = "This is a longer Label"
        return l
    }()
    
    var button: UIButton = {
        let l = UIButton()
        l.backgroundColor = UIColor.red
        l.setTitle("The Button", for: .normal)
        return l
    }()
    
    var divView: UIView = {
        let v = UIView()
        v.backgroundColor = UIColor.lightGray
        return v
    }()
    
    var headerView: UIView = {
        let v = UIView()
        v.backgroundColor = UIColor.blue
        return v
    }()
    
    // add our header view
    container.addSubview(headerView)
    
    // add div, label and button as subviews in headerView
    headerView.addSubview(divView)
    headerView.addSubview(label)
    headerView.addSubview(button)
    
    // disable Autoresizing Masks
    headerView.translatesAutoresizingMaskIntoConstraints = false
    divView.translatesAutoresizingMaskIntoConstraints = false
    label.translatesAutoresizingMaskIntoConstraints = false
    button.translatesAutoresizingMaskIntoConstraints = false
    
    var vcs: [NSLayoutConstraint]
    
    var views = ["divView": divView, "label": label, "button": button, "headerView": headerView]
    
    
    // init "header view" - we'll let its contents determine its width and height
    
    // these two formats will simply put the header view at 20,20
    vcs = NSLayoutConstraint.constraints(withVisualFormat:
        "H:|-20-[headerView]", options: [], metrics: nil, views: views)
    container.addConstraints(vcs)
    
    vcs = NSLayoutConstraint.constraints(withVisualFormat:
        "V:|-20-[headerView]", options: [], metrics: nil, views: views)
    container.addConstraints(vcs)
    
    
    // H: pin label 19 from left
    //    pin button 22 from right
    //    use 60 spacing between label and button
    //    width of label and button auto-determined by text
    //    also use .alignAllCenterY to vertically align them
    vcs = NSLayoutConstraint.constraints(withVisualFormat:
        "H:|-19-[label]-60-[button]-22-|", options: .alignAllCenterY, metrics: nil, views: views)
    headerView.addConstraints(vcs)
    
    // V: pin divView to the top, with a height of 10
    vcs = NSLayoutConstraint.constraints(withVisualFormat:
        "V:|[divView(10)]", options: [], metrics: nil, views: views)
    headerView.addConstraints(vcs)
    
    // V: pin label to bottom of divView (plus spacing of 20)
    //    using .alignAllLeft will pin the divView's left to the label's left
    vcs = NSLayoutConstraint.constraints(withVisualFormat:
        "V:[divView]-20-[label]", options: .alignAllLeft, metrics: nil, views: views)
    headerView.addConstraints(vcs)
    
    // V: pin button to bottom of divView (plus spacing of 20)
    //    using .alignAllRight will pin the divView's right to the button's right
    vcs = NSLayoutConstraint.constraints(withVisualFormat:
        "V:[divView]-20-[button]|", options: .alignAllRight, metrics: nil, views: views)
    headerView.addConstraints(vcs)