Justin
Justin

Reputation: 755

How to update swift Layout Anchors?

Trying to find a solution to update multiple constraints for multiple UI elements on an event. I have seen some examples of deactivating, making a change, then reactivating constraints, this method seems impractical for the 24 anchors I am working with.

One of my sets of changes:

ticketContainer.translatesAutoresizingMaskIntoConstraints = false
ticketContainer.topAnchor.constraintEqualToAnchor(self.topAnchor).active = true
ticketContainer.leftAnchor.constraintEqualToAnchor(self.rightAnchor, constant: 20).active = true
ticketContainer.widthAnchor.constraintEqualToConstant(200.0).active = true

ticketContainer.leftAnchor.constraintEqualToAnchor(self.leftAnchor, constant: 20).active = true
ticketContainer.widthAnchor.constraintEqualToConstant(100.0).active = true

Upvotes: 33

Views: 67574

Answers (5)

Rashid Latif
Rashid Latif

Reputation: 2901

Useful extension

extension UIView {

    //Note: Use `leadingAnchor and trailingAnchor` instead of `leftAnchor and rightAnchor`, it is going to help in RTL
    func addConstraint(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, paddingTop: CGFloat? = nil, paddingLeft: CGFloat? = nil, paddingBottom: CGFloat? = nil, paddingRight: CGFloat? = nil, width: CGFloat? = nil, height: CGFloat? = nil) {
        translatesAutoresizingMaskIntoConstraints = false
        //top constraint
        if let top = top {
            self.topAnchor.constraint(equalTo: top, constant: paddingTop ?? 0).isActive = true
        }
    
        // leading/left constraint
        if let leading = leading {
            self.leadingAnchor.constraint(equalTo: leading, constant: paddingLeft ?? 0).isActive = true
        }

        //  bottom constraint
        if let bottom = bottom {
            self.bottomAnchor.constraint(equalTo: bottom, constant: -(paddingBottom ?? 0) ).isActive = true
        }

        // trailing/right constraint
        if let trailing = trailing {
            self.trailingAnchor.constraint(equalTo: trailing, constant: -(paddingRight ?? 0)).isActive = true
        }

        //width constraint
        if let width = width {
            widthAnchor.constraint(equalToConstant: width).isActive = true
        }

        //height constraint
        if let height = height {
            heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }
}

How to use

 let myView = UIView()
 self.view.addSubview(myView)
 myView.backgroundColor = .yellow
 myView.addConstraint(top: self.view.safeAreaLayoutGuide.topAnchor, leading: self.view.safeAreaLayoutGuide.leadingAnchor, bottom: self.view.safeAreaLayoutGuide.bottomAnchor, trailing: self.view.safeAreaLayoutGuide.trailingAnchor, paddingTop: 100)

Upvotes: 0

JuliusBahr
JuliusBahr

Reputation: 91

I have found another solution. If you want to change an existing constraint added through Interface Builder you actually need to iterate over the constraints of the superview. This is at least true when you try to change the constant value of alignment constraints.

The below example shows the bottom alignment but I assume the same code would work for leading/trailing/top alignment.

    private func bottomConstraint(view: UIView) -> NSLayoutConstraint {
        guard let superview = view.superview else {
            return NSLayoutConstraint()
        }

        for constraint in superview.constraints {
            for bottom in [NSLayoutConstraint.Attribute.bottom, NSLayoutConstraint.Attribute.bottomMargin] {
                if constraint.firstAttribute == bottom && constraint.isActive && view == constraint.secondItem as? UIView {
                    return constraint
                }

                if constraint.secondAttribute == bottom && constraint.isActive && view == constraint.firstItem as? UIView {
                    return constraint
                }
            }
        }

        return NSLayoutConstraint()
    }

Upvotes: 0

Daniel Hall
Daniel Hall

Reputation: 13679

Have you tried saving the relevant constraints you created using the layout anchors to properties, and then just changing the constant? E.g.

var ticketTop : NSLayoutConstraint?

func setup() {
    ticketTop = ticketContainer.topAnchor.constraintEqualToAnchor(self.topAnchor, constant:100)
    ticketTop.active = true
}

func update() {
    ticketTop?.constant = 25
}

Possibly More Elegant

Depending on your taste for writing extensions, here is a possibly more elegant approach that doesn't use properties, but instead creates extension methods on NSLayoutAnchor and UIView to aid in more succinct usage.

First you would write an extension on NSLayoutAnchor like this:

extension NSLayoutAnchor {
    func constraintEqualToAnchor(anchor: NSLayoutAnchor!, constant:CGFloat, identifier:String) -> NSLayoutConstraint! {
        let constraint = self.constraintEqualToAnchor(anchor, constant:constant)
        constraint.identifier = identifier
        return constraint
    }
}

This extension allows you to set an identifier on the constraint in the same method call that creates it from an anchor. Note that Apple documentation implies that XAxis anchors (left, right, leading, etc.) won't let you create a constraint with YAxis anchors (top, bottom, etc.), but I don't observe this to actually be true. If you did want that type of compiler checking, you would need to write separate extensions for NSLayoutXAxisAnchor, NSLayoutYAxisAnchor, and NSLayoutDimension (for width and height constraints) that enforce the same-axis anchor type requirement.

Next you would write an extension on UIView to get a constraint by identifier:

extension UIView {
    func constraint(withIdentifier:String) -> NSLayoutConstraint? {
        return self.constraints.filter{ $0.identifier == withIdentifier }.first
    }
}

With these extensions in place, your code becomes:

func setup() {
    ticketContainer.topAnchor.constraintEqualToAnchor(anchor: self.topAnchor, constant:100, identifier:"ticketTop").active = true
}

func update() {
    self.constraint(withIdentifier:"ticketTop")?.constant = 25
}

Note that using constants or an enum instead of magic string names for the identifiers would be an improvement over the above, but I'm keeping this answer brief and focused.

Upvotes: 93

Justin
Justin

Reputation: 755

Positioning Single View

extension UIView {
    func add(view: UIView, left: CGFloat, right: CGFloat, top: CGFloat, bottom: CGFloat) {
        
        view.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(view)

        view.leftAnchor.constraint(equalTo: self.leftAnchor, constant: left).isActive = true
        view.rightAnchor.constraint(equalTo: self.rightAnchor, constant: right).isActive = true
        
        view.topAnchor.constraint(equalTo: self.topAnchor, constant: top).isActive = true
        view.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: bottom).isActive = true
        
    }
}

Usage

headerView.add(view: headerLabel, left: 20, right: 0, top: 0, bottom: 0)

Upvotes: 2

eg.rudolph
eg.rudolph

Reputation: 311

You can iterate over a view's constraints and check for matching items and anchors. Remember that the constraint will be on the view's superview unless it is a dimension constraint. I wrote some helper code that will allow you to find all constraints on one anchor of a view.

import UIKit

class ViewController: UIViewController {

    let label = UILabel()
    let imageView = UIImageView()

    override func viewDidLoad() {
        super.viewDidLoad()

        label.text = "Constraint finder"

        label.translatesAutoresizingMaskIntoConstraints = false
        imageView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        view.addSubview(imageView)

        label.topAnchor.constraint(equalTo: view.topAnchor, constant: 30).isActive = true
        label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20).isActive = true
        label.widthAnchor.constraint(greaterThanOrEqualToConstant: 50).isActive = true

        imageView.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
        imageView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 60).isActive = true
        imageView.widthAnchor.constraint(equalTo: label.widthAnchor).isActive = true
        imageView.heightAnchor.constraint(equalToConstant: 70).isActive = true

        print("Label's top achor constraints: \(label.constraints(on: label.topAnchor))")
        print("Label's width achor constraints: \(label.constraints(on: label.widthAnchor))")
        print("ImageView's width achor constraints: \(imageView.constraints(on: imageView.widthAnchor))")
    }

}

public extension UIView {

    public func constraints(on anchor: NSLayoutYAxisAnchor) -> [NSLayoutConstraint] {
        guard let superview = superview else { return [] }
        return superview.constraints.filtered(view: self, anchor: anchor)
    }

    public func constraints(on anchor: NSLayoutXAxisAnchor) -> [NSLayoutConstraint] {
        guard let superview = superview else { return [] }
        return superview.constraints.filtered(view: self, anchor: anchor)
    }

    public func constraints(on anchor: NSLayoutDimension) -> [NSLayoutConstraint] {
        guard let superview = superview else { return [] }
        return constraints.filtered(view: self, anchor: anchor) + superview.constraints.filtered(view: self, anchor: anchor)
    }

}

extension NSLayoutConstraint {

    func matches(view: UIView, anchor: NSLayoutYAxisAnchor) -> Bool {
        if let firstView = firstItem as? UIView, firstView == view && firstAnchor == anchor {
            return true
        }
        if let secondView = secondItem as? UIView, secondView == view && secondAnchor == anchor {
            return true
        }
        return false
    }

    func matches(view: UIView, anchor: NSLayoutXAxisAnchor) -> Bool {
        if let firstView = firstItem as? UIView, firstView == view && firstAnchor == anchor {
            return true
        }
        if let secondView = secondItem as? UIView, secondView == view && secondAnchor == anchor {
            return true
        }
        return false
    }

    func matches(view: UIView, anchor: NSLayoutDimension) -> Bool {
        if let firstView = firstItem as? UIView, firstView == view && firstAnchor == anchor {
            return true
        }
        if let secondView = secondItem as? UIView, secondView == view && secondAnchor == anchor {
            return true
        }
        return false
    }
}

extension Array where Element == NSLayoutConstraint {

    func filtered(view: UIView, anchor: NSLayoutYAxisAnchor) -> [NSLayoutConstraint] {
        return filter { constraint in
            constraint.matches(view: view, anchor: anchor)
        }
    }
    func filtered(view: UIView, anchor: NSLayoutXAxisAnchor) -> [NSLayoutConstraint] {
        return filter { constraint in
            constraint.matches(view: view, anchor: anchor)
        }
    }
    func filtered(view: UIView, anchor: NSLayoutDimension) -> [NSLayoutConstraint] {
        return filter { constraint in
            constraint.matches(view: view, anchor: anchor)
        }
    }

}

Upvotes: 14

Related Questions