Reputation: 755
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
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
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
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
}
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
Reputation: 755
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
}
}
headerView.add(view: headerLabel, left: 20, right: 0, top: 0, bottom: 0)
Upvotes: 2
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