Easier NSLayoutConstraint interactions
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(rawValue: 1) // 1
static let belowDefaultLow = LayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue - 1) // 2
static let defaultLow = LayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue)
static let aboveDefaultLow = LayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue + 1) // 3
static let belowDefaultHigh = LayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue - 1) // 4
static let defaultHigh = LayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue)
static let aboveDefaultHigh = LayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue + 1) // 5
static let maxNonRequired = LayoutPriority(rawValue: UILayoutPriority.required.rawValue - 1) // 6
static let required = 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.toUIKitSecond 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 - 1And 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()