Search code examples
iosswiftuibuttonnslayoutconstraint

Adding constraints to a custom UIButton doesn't work


Hi I have the following code:

class MyController {    
    override func viewDidLoad() {
        self.addButtons()

        super.viewDidLoad()
    }

    func addButtons() {
        let cancelButton = UIButton(type: .custom)
        let validateButton = UIButton(type: .custom)

        cancelButton.setImage(UIImage(named: "cancel_icon"), for: .normal)
        validateButton.setImage(UIImage(named: "validate_icon"), for: .normal)

        cancelButton.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
        validateButton.addTarget(self, action: #selector(validateAction), for: .touchUpInside)

        cancelButton.translatesAutoresizingMaskIntoConstraints = false
        validateButton.translatesAutoresizingMaskIntoConstraints = false

        self.view.addSubview(cancelButton)
        self.view.addSubview(validateButton)

        let leftCancel = NSLayoutConstraint(item: cancelButton, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 16)
        let bottomCancel = NSLayoutConstraint(item: cancelButton, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1, constant: 20)
        let widthCancel = NSLayoutConstraint(item: cancelButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 20)
        let heightCancel = NSLayoutConstraint(item: cancelButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 20)
        let rightValidate = NSLayoutConstraint(item: validateButton, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1, constant: 16)
        let bottomValidate = NSLayoutConstraint(item: validateButton, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1, constant: 20)
        let widthValidate = NSLayoutConstraint(item: validateButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 20)
        let heightValidate = NSLayoutConstraint(item: validateButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 20)
        NSLayoutConstraint.activate([leftCancel, bottomCancel, rightValidate, bottomValidate, widthCancel, heightCancel, widthValidate, heightValidate])

        self.view.layoutIfNeeded()
    }

    func cancelAction() {
        self.dismiss(animated: true, completion: nil)
    }

    func validateAction() {
        self.dismiss(animated: true, completion: nil)
    }
}

So basically I'm just adding two buttons with a width and height constraint, and I set the cancelButton to the bottom left and the validateButton to the bottom right.

My buttons don't appear. When I set the frame of the buttons this way, it works though:

cancelButton.frame = CGRect(x: 16, y: self.view.frame.height - 40, width: 20, height: 20) 
validateButton.frame = CGRect(x: self.view.frame.width - 36, y: self.view.frame.height - 40, width: 20, height: 20)

Does someone know what's wrong with my constraints? Thanks!


Solution

  • VisualFormat has its uses, but it also has its own quirks - such as flexible spacing between elements. Here's a solution without VisualFormat:

    //: Playground - noun: a place where people can play
    
    import UIKit
    
    import PlaygroundSupport
    
    class MyController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
    
            self.addButtons()
        }
    
        func addButtons() {
            let cancelButton = UIButton(type: .custom)
            let validateButton = UIButton(type: .custom)
    
            cancelButton.setImage(UIImage(named: "cancel_icon"), for: .normal)
            validateButton.setImage(UIImage(named: "validate_icon"), for: .normal)
    
            cancelButton.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
            validateButton.addTarget(self, action: #selector(validateAction), for: .touchUpInside)
    
            cancelButton.translatesAutoresizingMaskIntoConstraints = false
            validateButton.translatesAutoresizingMaskIntoConstraints = false
    
            self.view.addSubview(cancelButton)
            self.view.addSubview(validateButton)
    
            validateButton.backgroundColor = UIColor.red
            cancelButton.backgroundColor = UIColor.blue
    
            validateButton.setTitle("V", for: UIControlState())
            cancelButton.setTitle("C", for: UIControlState())
    
            let leftCancel = NSLayoutConstraint(item: cancelButton, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 16)
            let bottomCancel = NSLayoutConstraint(item: cancelButton, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1, constant: -20)
            let widthCancel = NSLayoutConstraint(item: cancelButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 20)
            let heightCancel = NSLayoutConstraint(item: cancelButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 20)
            let rightValidate = NSLayoutConstraint(item: validateButton, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1, constant: -16)
            let bottomValidate = NSLayoutConstraint(item: validateButton, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1, constant: -20)
            let widthValidate = NSLayoutConstraint(item: validateButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 20)
            let heightValidate = NSLayoutConstraint(item: validateButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 20)
    
            NSLayoutConstraint.activate([leftCancel, bottomCancel, rightValidate, bottomValidate, widthCancel, heightCancel, widthValidate, heightValidate])
    
            self.view.layoutIfNeeded()
        }
    
        func cancelAction() {
            //      self.dismiss(animated: true, completion: nil)
            print("cancel")
        }
    
        func validateAction() {
            //      self.dismiss(animated: true, completion: nil)
            print("validate")
        }
    }
    
    var vc = MyController()
    vc.view.backgroundColor = UIColor.green
    vc.view.frame = CGRect(x: 0, y: 0, width: 400, height: 400)
    
    PlaygroundPage.current.liveView = vc.view
    
    PlaygroundPage.current.needsIndefiniteExecution = true
    

    You should be able to copy/paste this into a Playground page to see the results. Note that I don't have your button images, so I "V" and "C" titles.

    The key difference is setting the Bottom and Trailing values to negative - that is, you want the Bottom of the Button to be (Bottom of containing view MINUS 20). Likewise with the Trailing edge of the Button to be -16 from the trailing edge of the containing view.