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

Technology

Power-Up Your Anchors - Part 1

By Pedro Carrasco
Pedro Carrasco
Event organiser, engineer, OSS contributor, and loyal fan of Adidas shoes
View All Posts
Power-Up Your Anchors - Part 1
Programmatically done Auto Layout is still the preferred way of implementing views by many developers. While there are amazing open-source frameworks, most of them differ from Apple’s anchor syntax. Therefore, by adding them to your project, you’ll raise the entry level complexity of your project and increase its learning curve. In this article, you’ll learn how to avoid adding an external dependency and create your own layer above NSLayoutAnchor to solve some of its issues.

Introduction

NSLayoutAnchor was first introduced by Apple in iOS 9.0 and it’s described as a "factory class for creating layout constraint objects using a fluent API”. Apple’s documentation also refers that NSLayoutAnchor usage is preferred when compared to NSLayoutConstraint: "use these constraints to programmatically define your layout using Auto Layout. Instead of creating NSLayoutConstraint …”. This is due to type checking and having a simpler and cleaner interface when compared to NSLayoutConstraint.

Improvements related to type checking are based in Apple’s decision to split NSLayoutAnchor into three different concepts, being:
  • NSLayoutXAxisAnchor for horizontal constraints.
  • NSLayoutYAxisAnchor for vertical constraints.
  • NSLayoutDimension for width and height constraints.
Apple’s documentation also mentions that "you never use the NSLayoutAnchor class directly. Instead, use one of its subclasses, based on the type of constraint you wish to create”. In short, you can never constrain anchors between the different subclasses shown above. However, you can still mess it up, as Apple states:

"While the NSLayoutAnchor class provides additional type checking, it is still possible to create invalid constraints. For example, the compiler allows you to constrain one view’s leadingAnchor with another view’s leftAnchor, since they are both NSLayoutXAxisAnchor instances. However, Auto Layout does not allow constraints that mix leading and trailing attributes with left or right attributes”.

First of all, take a look at the following code and try to identify some of NSLayoutAnchor’s boilerplate code.
// 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
According to this implementation, you should have found the following requirements:
  • You must set translatesAutoresizingMaskIntoConstraints to false for every view;
  • You must activate constraints by setting its property isActive to true or by using NSLayoutConstraint.activate();
  • You cannot set UILayoutPriority via a parameter and must instead create a variable.
Within this article, you will learn how to address these issues. However, keep in mind that this only applies to  Swift.

TranslatesAutor… Yes, that long property you always set to false

Here lies the first identified issue, and Apple is clear on why you always have to set it as false:

"If this property’s value is true, the system creates a set of constraints that duplicate the behaviour specified by the view’s authorising mask. This also lets you modify the view’s size and location using the view’s frame, bounds, or centre properties.

If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false".


This describes exactly what you want: use Auto Layout to calculate the size and position of your views dynamically. However, you don’t want to write this huge property for every single view, not even inside a forEach.

There are multiple approaches to solve this, but for now, you will be improving addSubview and adding a side-effect to it. To do so, create an UIView extension with the following code:
extension UIView {
  func addSubviewsUsingAutoLayout(_ views: UIView ...) {
    subviews.forEach {
      self.addSubview($0)
      $0.translatesAutoresizingMaskIntoConstraints = false
    }
  }
}
With this code, you will be able to send multiple views, set them all as subviews and set each one’s translatesAutoresizingMaskIntoConstraints to false all at once.
Now, instead of doing the following:
[logoImageView, welcomeLabel, dismissButton].forEach {
    self.addSubview($0)
    $0.translatesAutoresizingMaskIntoConstraints = false 
}
You will now have:
self.addSubviewsUsingAutoLayout(logoImageView, welcomeLabel, dismissButton)

What about the remaining issues?

Start by extending NSLayoutAnchor as follows:
extension NSLayoutAnchor {
  func test() {}
}
This will generate an error:


Prior to Swift 4, you were forced to extend each of NSLayoutAnchor’s subclasses, or constrain it, because it is a generic class, but now you can simply expose your extension to Objective-C.
@objc extension NSLayoutAnchor
If you try to compile this, you will notice that the error disappears. Your extension is now ready to use your own code, and that is exactly what you’re going to do.

Activate your constraints!

Setting isActive in every single anchor is excruciating. Even though NSLayoutConstraint.activate() might be considered a better option, according to Apple’s documentation, it still adds a lot of indentation.

One way of solving this would be to set isActive to true by default. You can achieve this with the following:
@objc extension NSLayoutAnchor {
  @discardableResult 
  func constrain(equalTo anchor: NSLayoutAnchor, 
                 with constant: CGFloat = 0.0, 
                 isActive: Bool = true) -> NSLayoutConstraint {
    let constraint = self.constraint(equalTo: anchor, constant: constant)
    constraint.isActive = isActive
    return constraint
  }
}
This function uses a Swift capability called default parameter. It allows isActive to be called as an optional argument. By default, it will always be set to true. However, in case you don’t want it active, you can set it to false.

By using @discardableResult, you will be returning an NSLayoutConstraint that you can safely ignore if you don’t need it. In case you never heard about this keyword, I’ve written an article titled "I Want to be Discardable” that addresses this.

Too many functions!

Currently, you can only support a relation of equalTo when you should also support greaterThanOrEqualTo and lessThanOrEqualTo.

According to Apple’s documentation, NSLayoutAnchor exposes 6 different functions- These are as follows:
  • func constraint(equalTo: NSLayoutAnchor) -> NSLayoutConstraint
  • func constraint(equalTo: NSLayoutAnchor, constant: CGFloat) -> NSLayoutConstraint
  • func constraint(greaterThanOrEqualTo: NSLayoutAnchor) -> NSLayoutConstraint
  • func constraint(greaterThanOrEqualTo: NSLayoutAnchor, constant: CGFloat) -> NSLayoutConstraint
  • func constraint(lessThanOrEqualTo: NSLayoutAnchor) -> NSLayoutConstraint
  • func constraint(lessThanOrEqualTo: NSLayoutAnchor, constant: CGFloat) -> NSLayoutConstraint
While Apple’s approach works, you can reduce the amount of functions in your interface with an enumeration approach for NSLayoutConstraint.Relation as in the following code:
@objc extension NSLayoutAnchor {
  @discardableResult 
  func constrain(_ relation: NSLayoutConstraint.Relation = .equal, 
                 to anchor: NSLayoutAnchor, 
                 with constant: CGFloat = 0.0, 
                 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.isActive = isActive
    return constraint
  }
}
If you try to use it:
let a = UIView()
let b = UIView()

a.addSubviewsUsingAutoLayout(b)

// Constraint set as equal to a.topAnchor
b.topAnchor.constrain(a.topAnchor)

// Constraint set as greater than or equal to a.bottomAnchor
b.bottomAnchor.constrain(.greaterThanOrEqual, to: a.bottomAnchor)

// Constraint set as less than or equal to a.bottomAnchor
b.leadingAnchor.constrain(.lessThanOrEqual, to: a.leadingAnchor)
Everything seems to be working well. But what if you try to apply a width constraint based on a constant? Add the following line of code to the previous example and rebuild:
b.widthAnchor.constrain(to: 50.0)
Oh no, you trigger another error:
But this time it is pretty easy to understand what’s happening. Currently, your function is expecting you to send an NSLayoutAnchor.

To solve this, the following are the two most obvious solutions:
  • Set the expecting anchor as an optional, NSLayoutAnchor?.
  • Provide a default parameter.
But none of these are optimal, because:
  • It would lead to some edge cases that aren’t supposed to be possible and wouldn’t even work. Therefore your interface would allow inconsistencies that didn’t exist before.
  • It isn’t possible to provide a valid default parameter to NSLayoutAnchor.
According to Apple’s documentation, you are only allowed to set a constraint without any relation to another anchor for NSLayoutDimension’s anchors.

To avoid missing cases that already exist, you should also have the option to apply a constraint between two NSLayouDimension anchors with a constant & multiplier.

In order to address this, you must enable this feature only for NSLayoutDimension’s anchors. Therefore, you are going to extend NSLayoutDimension with the following code:
extension NSLayoutDimension {
  @discardableResult
  func constrain(_ relation: NSLayoutConstraint.Relation = .equal,
                 to anchor: NSLayoutDimension,
                 with constant: CGFloat = 0.0,
                 multiplyBy multiplier: CGFloat = 1.0,
                 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.isActive = isActive
    return constraint
  }
  @discardableResult
  func constrain(_ relation: NSLayoutConstraint.Relation = .equal,
                 to constant: CGFloat = 0.0,
                 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.isActive = isActive
    return constraint
  }
}

You'll now be able to apply width and height constraints without any kind of relation to another anchor.

So, now that we've addressed setting translatesAutoresizingMaskIntroConstraints, relations and activating constraints, let's work on priorities and DRY in our part 2.

Shall we?




Related Articles