Swift Visual Format Language four buttons in centre

1k Views Asked by At

I would like to arrange four buttons with Visual Format Language around the central X an Y of a view without hard coding any points, preferring to scale with constraints.

I can only achieve a cluster of buttons to align to the bottom margin, how do I centre them with the spacing you see (e.g. ~20 points) without resorting to NSLayoutConstraint?

I did not place them in a stack, they are all separate buttons.
I read that stacks were not a good idea, but it seems like the logical way, otherwise they stretch out vertically.
Ideally I would like to use VFL to make a calculator UI but am trying this first.

@IBDesignable class images_and_constraints: UIButton {

    override init(frame: CGRect) {
        super.init(frame: frame)
        calcButtons()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        calcButtons()
    }

    private func calcButtons() {
        let calcPlus = UIButton()
        calcPlus.translatesAutoresizingMaskIntoConstraints = false
        calcPlus.setTitle("+", for: .normal)
        calcPlus.setTitleColor(UIColor.black, for: .normal)
        calcPlus.setTitleColor(UIColor.white, for: .highlighted)
        calcPlus.backgroundColor = UIColor.orange
        addSubview(calcPlus)

        let calcSubtract = UIButton()
        calcSubtract.translatesAutoresizingMaskIntoConstraints = false
        calcSubtract.setTitle("-", for: .normal)
        calcSubtract.setTitleColor(UIColor.black, for: .normal)
        calcSubtract.setTitleColor(UIColor.white, for: .highlighted)
        calcSubtract.backgroundColor = UIColor.orange
        addSubview(calcSubtract)

        let calcMultiply = UIButton()
        calcMultiply.translatesAutoresizingMaskIntoConstraints = false
        calcMultiply.setTitle("x", for: .normal)
        calcMultiply.setTitleColor(UIColor.black, for: .normal)
        calcMultiply.setTitleColor(UIColor.white, for: .highlighted)
        calcMultiply.backgroundColor = UIColor.orange
        addSubview(calcMultiply)

        let calcDivide = UIButton()
        calcDivide.translatesAutoresizingMaskIntoConstraints = false
        calcDivide.setTitle("/", for: .normal)
        calcDivide.setTitleColor(UIColor.black, for: .normal)
        calcDivide.setTitleColor(UIColor.white, for: .highlighted)
        calcDivide.backgroundColor = UIColor.orange
        addSubview(calcDivide)

        let views = ["calcPlus": calcPlus,
                     "calcSubtract": calcSubtract,
                     "calcMultiply": calcMultiply,
                     "calcDivide": calcDivide]

        NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcPlus]-[calcSubtract(==calcPlus)]-|",
                                                                   options: .alignAllBottom,
                                                                   metrics: nil,
                                                                   views: views))
        NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcMultiply]-[calcDivide(==calcMultiply)]-|",
                                                                   options: .alignAllTop,
                                                                   metrics: nil,
                                                                   views: views))
        NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:[calcSubtract]-[calcDivide(==calcSubtract)]-|",
                                                                   options: .alignAllCenterX,
                                                                   metrics: nil,
                                                                   views: views))
        NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:[calcSubtract]",
                                                                   options: .alignAllCenterX,
                                                                   metrics: nil,
                                                                   views: views))
    }
}
3

There are 3 best solutions below

0
On

Using VFL to center views requires trickery.
Look at this question and particularly this answer for the trick.

For the kind of layout you want, VFL is just not a good fit.
Just one NSLayoutConstraint in addition to VFL would solve it but since you're only interested in VFL, I would suggest you use the trick to center a container view that holds your buttons.

Solution:

func calcButtons() {
    //1. Create a container view that will contain your operator buttons
    let buttonContainerView = UIView()
    buttonContainerView.backgroundColor = UIColor.lightGray
    buttonContainerView.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(buttonContainerView)
    
    //Place it vertically in the center of the superview
    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:[superview]-(<=1)-[childView]",
                                                               options: .alignAllCenterX,
                                                               metrics: nil,
                                                               views: ["superview" : self,
                                                                       "childView" : buttonContainerView]))
    
    //Place it horizontally in the center of the superview + equal widths to superview
    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:[superview]-(<=1)-[childView(==superview)]",
                                                               options: .alignAllCenterY,
                                                               metrics: nil,
                                                               views: ["superview" : self,
                                                                       "childView" : buttonContainerView]))
    
    //2. Create your buttons as you were:
    
    //DRY Fix: Helper function to create button and add it to `buttonContainerView`
    func addButton(title: String, selector: Selector? = nil) -> UIButton {
        let button = UIButton()
        button.backgroundColor = UIColor.orange
        button.setTitle(title, for: .normal)
        button.setTitleColor(UIColor.black, for: .normal)
        button.setTitleColor(UIColor.white, for: .highlighted)
        
        //You might need this later cuz a button gotta do wat a button gotta do
        if let selector = selector {
            button.addTarget(self, action: selector, for: UIControlEvents.touchUpInside)
        }
        
        button.translatesAutoresizingMaskIntoConstraints = false
        buttonContainerView.addSubview(button)
        
        return button
    }
    
    let calcPlus = addButton(title: "+", selector: #selector(CalculatorView.add))
    let calcSubtract = addButton(title: "-")
    let calcMultiply = addButton(title: "x")
    let calcDivide = addButton(title: "/")
    
    let views = ["calcPlus": calcPlus,
                 "calcSubtract": calcSubtract,
                 "calcMultiply": calcMultiply,
                 "calcDivide": calcDivide]
    
    //Same as before
    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcPlus]-[calcSubtract(==calcPlus)]-|",
                                                               options: .alignAllBottom,
                                                               metrics: nil,
                                                               views: views))
    
    //Same as before
    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcMultiply]-[calcDivide(==calcMultiply)]-|",
                                                               options: .alignAllTop,
                                                               metrics: nil,
                                                               views: views))
    /*
     Same as before but this time we give a top constraint too
     i.e.
     "V:|-[calcSubtract]..."
     instead of
     "V:[calcSubtract]..."
     */
    //
    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[calcSubtract]-[calcDivide(==calcSubtract)]-|",
                                                               options: .alignAllCenterX,
                                                               metrics: nil,
                                                               views: views))
}
0
On

There is a new alternative to using VFL which is what I use in code now.

Layout Anchors

Each view has different anchors. leading, trailing, top, bottom, etc...

You can use these to create constraints for you...

NSLayoutConstraint.activate([
    viewB.leadingAnchor.constraint(equalTo: viewA.leadingAnchor, constant: 20),
    viewA.widthAnchor.constraint(equalTo: viewB.widthAnchor)
])

for example.

Stack View

In addition to that there is an even more modern approach which is to use UIStackView. This is a really useful view that takes away the need to add constraints and does it for you.

let stackView = UIStackView(arrangedSubViews: [viewA, viewB])
stackView.spacing = 20
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fillEqually

You can also nest stack views to create more complex layouts.

Definitely worth looking in to...

https://developer.apple.com/documentation/uikit/uistackview?changes=_6

Creating your layout

let upperStackView = UIStackView(arrangedSubviews: [topLeft, topRight])
upperStackView.axis = .horizontal
upperStackView.distribution = .fillEqually
upperStackView.spacing = 20

let lowerStackView = UIStackView(arrangedSubviews: [bottomLeft, bottomRight])
lowerStackView.axis = .horizontal
lowerStackView.distribution = .fillEqually
lowerStackView.spacing = 20

let mainStackView = UIStackView(arrangedSubviews: [upperStackView, lowerStackView])
mainStackView.axis = .vertical
mainStackView.distribution = .fillEqually
mainStackView.spacing = 20

view.addSubview(mainStackView)

NSLayoutConstraint.activate([
    mainStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    mainStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    mainStackView.widthAnchor.constraint(equalToConstant: 200),
    mainStackView.heightAnchor.constraint(equalToConstant: 200),
])

Why not VFL?

While VFL was a nice first attempt at AutoLayout, I feel that Apple has moved away from it now and are moving towards these more succinct methods of creating AutoLayout constraints.

It still allows you to think in constraints while writing code but provides a slightly more modern approach.

Of course... you can also create UIStackView in Interface Builder also :D

0
On

In the end I decided on NSLayoutConstraint.activate of which each button would be reliant on the one before it (rows), with the leading (far left for left-to-right readers) button constrained to the one above it.

calculatriceButtons["7"]!.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 1.0),
calculatriceButtons["7"]!.topAnchor.constraint(equalTo: calculatriceButtons["C"]!.bottomAnchor, constant: 1.0),

This was the best way to assure the buttons scaled on all devices.