This website uses cookies. By using the website you agree with our use of cookies. Know more

Technology

Power-Up Your Anchors - Part 2

By Pedro Carrasco
Pedro Carrasco
Event organiser, engineer, OSS contributor, and loyal fan of Adidas shoes
View All Posts
Power-Up Your Anchors - Part 2

We've addressed setting translatesAutoresizingMaskIntoConstraints, relations and activating constraints in our part 1 (if you haven't read it yet, please be invited to read it here), and now let's work on priorities and DRY.

Priorities

To set a constraint’s priority in the current model, with anchors, you would need to do the following:
let bWidth = b.widthAnchor.constraint(equalToConstant: 50.0)
		bWidth.priority = .defaultHigh
		// or if you want between .defaultHigh and .required
		bWidth.priority = UILayoutPriority(UILayoutPriority.defaultHigh.rawValue + 1)
A good way to avoid being forced to assign the constraint to a property is adding this option to your functions. Also, with a default value of .required, you don’t need to type it in most scenarios.

Replace your functions with the following:
@objc extension NSLayoutAnchor {
		  @discardableResult
		  func constrain(_ relation: NSLayoutConstraint.Relation = .equal,
		                 to anchor: NSLayoutAnchor,
		                 with constant: CGFloat = 0.0,
		                 prioritizeAs priority: UILayoutPriority = .required,
		                 isActive: Bool = true) -> NSLayoutConstraint {
		    let constraint: NSLayoutConstraint
		    switch relation {
		      case .equal:
		        constraint = self.constraint(equalTo: anchor, constant: constant)
		      case .greaterThanOrEqual:
		        constraint = self.constraint(greaterThanOrEqualTo: anchor, constant: constant)
		      case .lessThanOrEqual:
		        constraint = self.constraint(lessThanOrEqualTo: anchor, constant: constant)
		    }
		    constraint.priority = priority
		    constraint.isActive = isActive
		    return constraint
		  }
		}
		extension NSLayoutDimension {
		  @discardableResult
		  func constrain(_ relation: NSLayoutConstraint.Relation = .equal,
		                 to anchor: NSLayoutDimension,
		                 with constant: CGFloat = 0.0,
		                 multiplyBy multiplier: CGFloat = 1.0,
		                 prioritizeAs priority: UILayoutPriority = .required,
		                 isActive: Bool = true) -> NSLayoutConstraint {
		    let constraint: NSLayoutConstraint
		    switch relation {
		      case .equal:
		        constraint = self.constraint(equalTo: anchor, multiplier: multiplier, constant: constant)
		      case .greaterThanOrEqual:
		        constraint = self.constraint(greaterThanOrEqualTo: anchor, multiplier: multiplier, constant: constant)
		      case .lessThanOrEqual:
		        constraint = self.constraint(lessThanOrEqualTo: anchor, multiplier: multiplier, constant: constant)
		        }
		    constraint.priority = priority
		    constraint.isActive = isActive
		    return constraint
		  }
		  @discardableResult
		  func constrain(_ relation: NSLayoutConstraint.Relation = .equal,
		                 to constant: CGFloat = 0.0,
		                 prioritizeAs priority: UILayoutPriority = .required,
		                 isActive: Bool = true) -> NSLayoutConstraint {
		    let constraint: NSLayoutConstraint
		    switch relation {
		      case .equal:
		        constraint = self.constraint(equalToConstant: constant)
		      case .greaterThanOrEqual:
		        constraint = self.constraint(greaterThanOrEqualToConstant: constant)
		      case .lessThanOrEqual:
		        constraint = self.constraint(lessThanOrEqualToConstant: constant)
		    }
		    constraint.priority = priority
		    constraint.isActive = isActive
		    return constraint
		  }
		}

What if you wanted a priority between .defaultHigh and .required? The current way doesn’t look very clean. In order to improve it, you are going to extend NSLayoutPriority with the following:

extension UILayoutPriority {
		  static func +(lhs: UILayoutPriority, rhs: Float) -> UILayoutPriority {
		    return UILayoutPriority(lhs.rawValue + rhs)
		  }
		  static func -(lhs: UILayoutPriority, rhs: Float) -> UILayoutPriority {
		    return UILayoutPriority(lhs.rawValue - rhs)
		  }
		}

And now you’ll be able to do the following:

bWidth.priority = .defaultHigh + 1

DRY

DRY stands for "Don’t Repeat Yourself,” and it is often seen as a golden rule in software development.

You are currently repeating yourself when setting isActive and priority. In order to follow this pattern, you will move this to an NSLayoutConstraint extension.

Start by adding the following function to your extension:
func set(priority: UILayoutPriority, isActive: Bool) { 
		    self.priority = priority
		    self.isActive = isActive
		}
And now update your functions with the following:
@objc extension NSLayoutAnchor {
		  @discardableResult
		  func constrain(_ relation: NSLayoutConstraint.Relation = .equal,
		                 to anchor: NSLayoutAnchor,
		                 with constant: CGFloat = 0.0,
		                 prioritizeAs priority: UILayoutPriority = .required,
		                 isActive: Bool = true) -> NSLayoutConstraint {
		    var constraint: NSLayoutConstraint
		    switch relation {
		      case .equal:
		        constraint = self.constraint(equalTo: anchor, constant: constant)
		      case .greaterThanOrEqual:
		        constraint = self.constraint(greaterThanOrEqualTo: anchor, constant: constant)
		      case .lessThanOrEqual:
		        constraint = self.constraint(lessThanOrEqualTo: anchor, constant: constant)
		    }
		    constraint.set(priority: priority, isActive: isActive)
		    return constraint
		  }
		}
		extension NSLayoutDimension {
		  @discardableResult
		  func constrain(_ relation: NSLayoutConstraint.Relation = .equal,
		                 to anchor: NSLayoutDimension,
		                 with constant: CGFloat = 0.0,
		                 multiplyBy multiplier: CGFloat = 1.0,
		                 prioritizeAs priority: UILayoutPriority = .required,
		                 isActive: Bool = true) -> NSLayoutConstraint {
		    let constraint: NSLayoutConstraint
		    switch relation {
		      case .equal:
		        constraint = self.constraint(equalTo: anchor, multiplier: multiplier, constant: constant)
		      case .greaterThanOrEqual:
		        constraint = self.constraint(greaterThanOrEqualTo: anchor, multiplier: multiplier, constant: constant)
		      case .lessThanOrEqual:
		        constraint = self.constraint(lessThanOrEqualTo: anchor, multiplier: multiplier, constant: constant)
		        }
		    constraint.set(priority: priority, isActive: isActive)
		    return constraint
		  }
		  @discardableResult
		  func constrain(_ relation: NSLayoutRelation = .equal,
		                 to constant: CGFloat = 0.0,
		                 prioritizeAs priority: UILayoutPriority = .required,
		                 isActive: Bool = true) -> NSLayoutConstraint {
		    var constraint: NSLayoutConstraint
		    switch relation {
		      case .equal:
		        constraint = self.constraint(equalToConstant: constant)
		      case .greaterThanOrEqual:
		        constraint = self.constraint(greaterThanOrEqualToConstant: constant)
		      case .lessThanOrEqual:
		        constraint = self.constraint(lessThanOrEqualToConstant: constant)
		    }
		    constraint.set(priority: priority, isActive: isActive)
		    return constraint   
		  }
		}

What have you done?

At the beginning of this article, you spotted some problems related to anchors. Now you can proudly check that you’ve developed solutions to each of them.
  • Having to set translatesAutoresizingMaskIntoConstraints to false for every view that isn’t loaded from a NIB.
    • You’ve created a new addSubview function that supports sending multiple UIView
  • Activating constraints by setting its property isActive to true, or using NSLayoutConstraint.activate()
  • Setting UILayoutPriority via parameter is not supported and requires you to create a variable.
    • NSLayoutAnchor extensions work together to solve these two problems.
  • Interoperability has its costs to  NSLayoutAnchor because it doesn’t allow it to take advantage of some Swift capabilities.
    • You’ve reduced the number of functions needed with an enum based approach and adopted default parameters in your interface.
Everything is looking good on paper. However, how does it look in practice?

How does it look?

Before showcasing your new and fresh powered-up anchors, take another look at the initial example shown in this article:
// Subviews
		let logoImageView = UIImageView()
		let welcomeLabel = UILabel()
		let dismissButton = UIButton()
		// Add Subviews & Set view's translatesAutoresizingMaskIntoConstraints to false
		[logoImageView, welcomeLabel, dismissButton].forEach { 
		  self.addSubview($0)
		  $0.translatesAutoresizingMaskIntoConstraints = false 
		}
		// Set Constraints
		logoImageView.topAnchor.constraint(equalTo: topAnchor, constant: 12).isActive = true
		logoImageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
		logoImageView.widthAnchor.constraint(equalToConstant: 50).isActive = true
		logoImageView.heightAnchor.constraint(equalToConstant: 50).isActive = true
		dismissButton.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 12).isActive = true
		dismissButton.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -12).isActive = true
		dismissButton.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
		let dismissButtonWidth = dismissButton.widthAnchor.constraint(equalToConstant: 320)
		dismissButtonWidth.priority = UILayoutPriority(UILayoutPriority.defaultHigh.rawValue + 1)
		dismissButtonWidth.isActive = true
		welcomeLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 12).isActive = true
		welcomeLabel.bottomAnchor.constraint(greaterThanOrEqualTo: dismissButton.topAnchor, constant: 12).isActive = true
		welcomeLabel.leadingAnchor.constraint(equalTo: dismissButton.leadingAnchor).isActive = true
		welcomeLabel.trailingAnchor.constraint(equalTo: dismissButton.trailingAnchor).isActive = true
Now you can achieve the same result in a much cleaner way with your extensions:

// Subviews
		let logoImageView = UIImageView()
		let welcomeLabel = UILabel()
		let dismissButton = UIButton()
		// Add Subviews & Set view's translatesAutoresizingMaskIntoConstraints to false
		addSubviewsUsingAutoLayout(logoImageView, welcomeLabel, dismissButton)
		// Set Constraints
		logoImageView.topAnchor.constrain(to: topAnchor, with: 12)
		logoImageView.centerXAnchor.constrain(to: centerXAnchor)
		logoImageView.widthAnchor.constrain(to: 50)
		logoImageView.heightAnchor.constrain(to: 50)
		dismissButton.leadingAnchor.constrain(.greaterThanOrEqual, to: leadingAnchor, with: 12)
		dismissButton.trailingAnchor.constrain(.lessThanOrEqual, to: trailingAnchor, with: -12)
		dismissButton.bottomAnchor.constrain(to: bottomAnchor)
		dismissButton.widthAnchor.constrain(to: 320, prioritizeAs: .defaultHigh + 1)
		welcomeLabel.topAnchor.constrain(to: logoImageView.bottomAnchor, with: 12)
		welcomeLabel.bottomAnchor.constrain(.greaterThanOrEqual, to: dismissButton.topAnchor, with: 12)
		welcomeLabel.leadingAnchor.constrain(to: dismissButton.leadingAnchor)
		welcomeLabel.trailingAnchor.constrain(to: dismissButton.trailingAnchor)
As you can see, it is a lot easier to read and understand what each anchor is doing. You were also able to decrease the amount of code you needed to write.

Future Improvements

While this will simplify the usage of Auto Layout programmatically, there are some details missing. The following functions aren’t covered:
  • constraintEqualToSystemSpacingBelow(NSLayoutYAxisAnchor, multiplier: CGFloat)
  • constraintGreaterThanOrEqualToSystemSpacingBelow(NSLayoutYAxisAnchor, multiplier: CGFloat)
  • constraintLessThanOrEqualToSystemSpacingBelow(NSLayoutYAxisAnchor, multiplier: CGFloat)
  • constraintEqualToSystemSpacingAfter(NSLayoutXAxisAnchor, multiplier: CGFloat)
  • constraintGreaterThanOrEqualToSystemSpacingAfter(NSLayoutXAxisAnchor, multiplier: CGFloat)
  • constraintLessThanOrEqualToSystemSpacingAfter(NSLayoutXAxisAnchor, multiplier: CGFloat)
If requested, we can review this in a follow-up article.

The End

If you’ve managed to fully read this and reach the end, congratulations on powering your anchors!

In the end, what’s your opinion of it? Did you know it already, or was it something new to you? Let me know, alongside with your questions, by sending feedback on Twitter.

Thanks for reading!


Last but not least, I would like to praise Ana Filipa Ferreira, João Pereira, José Figueiredo, Pedro Eusébio and Tiago Silva for their outstanding support.
Related Articles