Search code examples
iosswiftautolayoutnslayoutconstraintios-autolayout

Centering button with fixed width breaks NSLayoutConstraint


I have an interesting problem. My app is entirely managed through code instead of storyboards. I have a UIViewController which is presented programmatically. It creates a button and constraints for it.

This is what the controller code looks like.

import Foundation
import UIKit

class CreateViewController: UIViewController {

    lazy var button: UIButton = {
        let button =  UIButton()
        button.layer.bounds = CGRect(x: 0, y: 0, width: 50, height: 50)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = .green
        return button
    }()

    func setupViews() {
        self.view.addSubview(button)
    }

    func setupConstraints() {
        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[button]-100-|", options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: ["button": button]))
        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[button(50)]-|", options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: ["button": button]))
    }

    override func viewDidLoad() {
        setupViews()
        setupConstraints()
    }
}

It throws this error.

(
    "<NSLayoutConstraint:0x60000288bb60 UIButton:0x7fa1ab422680.leading == UILayoutGuide:0x6000032da760'UIViewLayoutMarginsGuide'.leading NSSpace(0)   (active)>",
    "<NSLayoutConstraint:0x60000288bb10 UIButton:0x7fa1ab422680.width == 50   (active)>",
    "<NSLayoutConstraint:0x60000288d7c0 UILayoutGuide:0x6000032da760'UIViewLayoutMarginsGuide'.trailing == UIButton:0x7fa1ab422680.trailing NSSpace(0)   (active)>",
    "<NSLayoutConstraint:0x6000028fe990 'UIView-Encapsulated-Layout-Width' UIView:0x7fa1ab4224a0.width == 375   (active)>",
    "<NSLayoutConstraint:0x60000288fe30 'UIView-leftMargin-guide-constraint' H:|-(16)-[UILayoutGuide:0x6000032da760'UIViewLayoutMarginsGuide'](LTR)   (active, names: '|':UIView:0x7fa1ab4224a0 )>",
    "<NSLayoutConstraint:0x60000288d180 'UIView-rightMargin-guide-constraint' H:[UILayoutGuide:0x6000032da760'UIViewLayoutMarginsGuide']-(16)-|(LTR)   (active, names: '|':UIView:0x7fa1ab4224a0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000288d7c0 UILayoutGuide:0x6000032da760'UIViewLayoutMarginsGuide'.trailing == UIButton:0x7fa1ab422680.trailing NSSpace(0)   (active)>

My aim here is pretty simple. I want the button to be 100 pixels above the bottom, to be centered horizontally, and to have a fixed width and height.

This is how the controller is being instantiated. It exists inside of a

        let nav2 = UINavigationController()
        let create = CreateViewController()
        nav2.viewControllers = [create]
        nav2.navigationBar.isTranslucent = false
        ...
        let tabs = UITabBarController()
        tabs.viewControllers = [..., nav2, ...]

        tabs.delegate = self

Any help in understanding why these constraints are being violated would be greatly appreciated! If any part of the question is unclear, just let me know.

FYI the resulting view looks like this:

the button is not centered here


Solution

  • Centering an element with Visual Format Language is difficult.

    You can easily do it using .centerXAnchor:

    class CreateViewController: UIViewController {
    
        lazy var button: UIButton = {
            let button =  UIButton()
            // next line is not needed
    //      button.layer.bounds = CGRect(x: 0, y: 0, width: 50, height: 50)
            button.translatesAutoresizingMaskIntoConstraints = false
            button.backgroundColor = .green
            return button
        }()
    
        func setupViews() {
            self.view.addSubview(button)
        }
    
        func setupConstraints() {
    
            NSLayoutConstraint.activate([
    
                // button width of 50
                button.widthAnchor.constraint(equalToConstant: 50.0),
    
                // button height of 50
                button.heightAnchor.constraint(equalToConstant: 50.0),
    
                // bottom of button 100-pts from bottom of view
                // note: contant is negative!
                button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100),
    
                // center the button horizontally
                button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    
                ])
    
        }
    
        override func viewDidLoad() {
            setupViews()
            setupConstraints()
        }
    }