Easier NSLayoutConstraint interactions

:  ~ 3 min read

In Swift 4 UILayoutPriority has become a struct, with an initializer and a rawValue, instead of being a rawValue of Float itself. This means that simple assignments became slightly harder:

let constraint = someView.leadingAnchor.constraint(equalTo: otherView.leadingAnchor)
// Swift < 4
constraint.priority = 999
// Swift 4
constraint.priority = UILayoutPriority(rawValue: 999)

Besides this, I've always had a pet peeve – activating constraints that require priority manipulation:

NSLayoutConstraint.activate([
  someView.leadingAnchor.constraint(equalTo: otherView.leadingAnchor),
  someView.trailingAnchor.constraint(equalTo: otherView.trailingAnchor)
])

Setting a priority for the leadingAnchor requires a couple of extra steps:

let leadingConstraint = someView.leadingAnchor.constraint(equalTo: otherView.leadingAnchor)
leadingConstraint.priority = UILayoutPriority(rawValue: 999)

NSLayoutConstraint.activate([
  leading,
  someView.trailingAnchor.constraint(equalTo: otherView.trailingAnchor)
])

What if we could solve these two problems almost in one go? At first I thought of creating an enum for this, but I decided to follow Apple's approach and created a struct, so let's do that:

struct LayoutPriority {
	
  var rawValue: Float
  var toUIKit: UILayoutPriority {
    return UILayoutPriority(rawValue: rawValue)
  }
	
  static let minNonZero: LayoutPriority = LayoutPriority(rawValue: 1) // 1
  static let belowDefaultLow: LayoutPriority = LayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue - 1) // 2
  static let defaultLow: LayoutPriority = LayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue)
  static let aboveDefaultLow: LayoutPriority = LayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue + 1) // 3
  static let belowDefaultHigh: LayoutPriority = LayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue - 1) // 4
  static let defaultHigh: LayoutPriority = LayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue)
  static let aboveDefaultHigh: LayoutPriority = LayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue + 1) // 5
  static let maxNonRequired: LayoutPriority = LayoutPriority(rawValue: UILayoutPriority.required.rawValue - 1) // 6
  static let required: LayoutPriority = LayoutPriority(rawValue: UILayoutPriority.required.rawValue) // 7
	
	
  // MARK: - Init
	
  init(rawValue: Float) {
    self.rawValue = rawValue
  }
	
}

It contains all the values from the UIKit version, and a few convenience ones (1-7), for the most used custom scenarios. For example, if you have two views with equal compressionResistance (rawValue = 750) and the Auto Layout engine can't solve the constraints, it might require you to change one of the views' compressionResistance to 749 or 751 to break the tie; now we have belowDefaultHigh and belowDefaultLow for that.

This brings us one step closer to our final goal – less typing, more autocomplete:

let constraint = someView.leadingAnchor.constraint(equalTo: otherView.leadingAnchor)
constraint.priority = LayoutPriority.maxNonRequired.toUIKit

Second step would be to create an NSLayoutConstraint extension:

extension NSLayoutConstraint {
  
  // This is probably not the best name, since it mutates `self`, but I thought it's better than add/set/change.
  @discardableResult
  func with(priority: LayoutPriority) -> NSLayoutConstraint {
    self.priority = UILayoutPriority(rawValue: priority.rawValue)
    return self
  }
  
}

And we reached our final goal, of setting priorities directly inline:

NSLayoutConstraint.activate([
  someView.leadingAnchor.constraint(equalTo: otherView.leadingAnchor)
    .with(priority: .maxNonRequired),
  someView.trailingAnchor.constraint(equalTo: otherView.trailingAnchor),
    .with(priority: LayoutPriority(rawValue: 123))
])

We can go an extra mile and add a property to the NSLayoutConstraint extension, that can bridge between LayoutProperty and UILayoutProperty:

extension NSLayoutConstraint {
  
  var layoutPriority: LayoutPriority {
    get { return LayoutPriority(rawValue: priority.rawValue) }
    set { priority = UILayoutPriority(rawValue: newValue.rawValue) }
  }
  
}

Although this won't bring that much value, since we have the previous helper function, it does bring some, for example when changing between two layouts, based on a condition:

if layout1Condition {
  someConstraint.layoutPriority = .minNonZero
  someOtherConstraint.layoutPriority = .maxNonRequired
}
else {
  someConstraint.layoutPriority = .maxNonRequired
  someOtherConstraint.layoutPriority = .minNonZero
}

Lastly, we can take this one step even further, by adding operators on our LayoutPriority:

extension LayoutPriority {
	
  static func -(lhs: LayoutPriority, rhs: Float) -> LayoutPriority {
    return LayoutPriority(rawValue: lhs.rawValue - rhs)
  }
  
  static func -=(lhs: inout LayoutPriority, rhs: Float) {
    lhs = LayoutPriority(rawValue: lhs.rawValue - rhs)
  }
  
  // ...
  
  }

Now we can more easily handle constraints dependent on other:

let constraint = someView.leadingAnchor.constraint(equalTo: otherView.leadingAnchor)
constraint.layoutPriority = .belowDefaultHigh

let otherConstraint = otherView.leadingAnchor.constraint(equalTo: otherView.otherAnchor)
otherConstraint.layoutPriority = constraint.layoutPriority - 1

And why not have two more additions to our NSLayoutConstraint extension?

extension NSLayoutConstraint {
  
  @discardableResult
  func activate() -> NSLayoutConstraint {
    isActive = true
    return self
  }
  
  @discardableResult	
  func deactivate() -> NSLayoutConstraint {
    isActive = false
    return self
  }
  
}

These won't bring much value, either, unless you like to chain things, like me:

let constraint = someView.leadingAnchor.constraint(equalTo: otherView.leadingAnchor)
constraint.priority = UILayoutPriority(rawValue: UILayoutPriority.defaultHight.rawValue - 1)
constraint.isActive = true

// vs

someView.leadingAnchor
  .constraint(equalTo: otherView.leadingAnchor)
  .with(priority: .defaultHigh - 1)
  .activate()