Search code examples
swiftautolayoutanchornslayoutconstraint

Difference between using NSLayoutConstraints initializer compared to Anchors for setting constraints


Why do some developers add constraints like this:

NSLayoutConstraint(item: myView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: 20.0).isActive = true

And some like this:

myView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 20).isActive = true

They basically do the same thing... right? So what is the difference between them? Why should one be used instead of the other? Is there a performance difference for using one over the other?

At the place I worked our iOS lead was using the NSLayoutConstraint initialization way exclusively and everyone was forced to do the same to have more consistency and readability throughout the entire code, I like both ways, I just want to know if there was/is ever any benefit of using one over the other? Or are the differences just based on preference?


Solution

  • In large part, it is new syntax and readability, but there are still some things that you can do with NSLayoutConstraint(...) that you cannot do the "new" way.

    For example, let's take a simple task of adding a UILabel centered horizontally, 40-pts from the bottom. Each of the following examples will begin with this:

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let myView = UILabel()
        myView.backgroundColor = .green
        myView.text = "Hello"
        
        myView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myView)
        
        // add constraints....
    
    }
    

    So, our first method looks like this:

            // center horizontally
            NSLayoutConstraint(item: myView,
                               attribute: .centerX,
                               relatedBy: .equal,
                               toItem: view,
                               attribute: .centerX,
                               multiplier: 1.0,
                               constant: 0.0).isActive = true
            
            // bottom = 40-pts from view Bottom
            NSLayoutConstraint(item: myView,
                               attribute: .bottom,
                               relatedBy: .equal,
                               toItem: view,
                               attribute: .bottom,
                               multiplier: 1.0,
                               constant: -40.0).isActive = true
    

    We can get the exact same results using this syntax, which would generally be considered more "readable":

            // center horizontally
            myView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
            // bottom = 40-pts from view Bottom
            myView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40.0).isActive = true
    

    Now, we'll usually have many more constraints to set, so we can make it even more readable with this (eliminates the .isActive = true at the end of every line):

            NSLayoutConstraint.activate([
                // center horizontally
                myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                // bottom = 40-pts from view Bottom
                myView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40.0),
            ])
    

    So... What happens if we throw in just a little complexity, such as saying "Keep the Bottom of the Label 20% from the bottom of the view"?


    To stick with the "new, more readable" syntax, we have a couple options...

    1 - constrain the bottom of the Label to the bottom of the view, wait until layout is finished - so we know the height of the view - and then set the .constant on the bottom anchor to -(view height * 0.2). That will work, but we have to re-calculate every time the label's superview changes (such as on device rotation).

    2 - add a UIView as a "bottom spacer":

            // add a hidden UIView for bottom "space"
            let spacerView = UIView()
            spacerView.isHidden = true
            view.addSubview(spacerView)
            spacerView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                // spacerView at bottom, height = 20% of view height
                spacerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
                spacerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2),
    
                // center horizontally
                myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                // bottom = spacerView Top
                myView.bottomAnchor.constraint(equalTo: spacerView.topAnchor, constant: 0.0),
            ])
    

    That works, and handles superview size changes, but we've added another view to the view hierarchy. For this simple example, no big deal, but we probably don't want to add a bunch of them for a complex layout.

    3 - add a UILayoutGuide as a "bottom spacer":

            // add a UILayoutGuide for bottom "space"
            let spacerGuide = UILayoutGuide()
            view.addLayoutGuide(spacerGuide)
            NSLayoutConstraint.activate([
                // spacerGuide at bottom, height = 20% of view height
                spacerGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
                spacerGuide.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2),
                
                // center horizontally
                myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                // bottom = spacerGuide Top
                myView.bottomAnchor.constraint(equalTo: spacerGuide.topAnchor, constant: 0.0),
            ])
    

    Accomplishes the same thing, but now we're using a non-rendering UI element so we're not weighing down the view hierarchy.

    4 - Use the NSLayoutConstraint(...) syntax, and avoid all of that:

            // center horizontally
            NSLayoutConstraint(item: myView,
                               attribute: .centerX,
                               relatedBy: .equal,
                               toItem: view,
                               attribute: .centerX,
                               multiplier: 1.0,
                               constant: 0.0).isActive = true
            
            // bottom = 80% of view bottom (leaves 20% space at bottom)
            NSLayoutConstraint(item: myView,
                               attribute: .bottom,
                               relatedBy: .equal,
                               toItem: view,
                               attribute: .bottom,
                               multiplier: 0.8,
                               constant: 0.0).isActive = true
        }
    

    So, for most situations, it is a matter of preference and/or consistency, but you will find the occasional case where there is a difference.