How to make Auto Layout more convenient in iOS

Khoa Pham
Apr 25 · 15 min read
Thanks to unDraw for great illustrations!

Positioning a view before Auto Layout

When I first started iOS programming in early 2014, I read a book about Auto Layout and that book detailed lots of scenarios that completely confused me. It didn’t take long until I tried Auto Layout in an app and I realized it w as so simple. In its simplest sense, a view needs a position and a size to be correctly shown on the screen, everything else is just extra. In Auto Layout’s term, we need to specify enough constraints to position and size the view.

Manual layout using CGRect

If we take a look back at the way we do a manual layout with the frame, there are origins and sizes. For example, here is how to position a red box that stretches accordingly with the width of the view controller.

Why is view size correct in viewDidLoad

viewDidLoad is definitely not the recommended way to do manual layout but there are times we still see that its view is correctly sized and fills the screen. This is when we need to read View Management in UIViewController more thoroughly:

Autoresizing masks

box.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin]
  • Center horizontally [.flexibleLeftMargin, .flexibleRightMargin]
  • Vertically fixed distance from the top: [.flexibleBottomMargin]
  • Center vertically [.flexibleTopMargin, .flexibleBottomMargin]

Auto Layout to the rescue

Auto Layout, together with dynamic text and size classes, are recommended ways to build adaptive user interfaces as there the number of iOS devices with different screen sizes grows.

A constraint-based layout system

Auto Layout is described via NSLayoutConstraint by defining constraints between 2 objects. Here is the simple formula to remember:

item1.attribute1 = multiplier × item2.attribute2 + constant

translatesAutoresizingMaskIntoConstraints

If we run the above code, we will get into the popular warning message regarding translatesAutoresizingMaskIntoConstraints

[LayoutConstraints] Unable to simultaneously satisfy constraints.
 Probably at least one of the constraints in the following list is one you don't want. 
 Try this: 
  (1) look at each constraint and try to figure out which you don't expect; 
  (2) find the code that added the unwanted constraint or constraints and fix it. 
 (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSAutoresizingMaskLayoutConstraint:0x600003ef2300 h=--& v=--& UIView:0x7fb66c5059f0.midX == 0   (active)>",
    "<NSLayoutConstraint:0x600003e94b90 H:|-(20)-[UIView:0x7fb66c5059f0](LTR)   (active, names: '|':UIView:0x7fb66c50bce0 )>"
)
box.translatesAutoresizingMaskIntoConstraints = false

Visual Format Language

The Visual Format Language lets you use ASCII-art like strings to define your constraints. I see it is used in some code bases so it’s good to know it.

addConstraint and activate

This may seem trivial but I see in modern code bases, there is still usage of addConstraint . This was old and hard to use, as we must find the nearest common ancestor view of the 2 views that envolve in Auto Layout.

NSLayoutAnchor

Starting with macOS 10.11 and iOS 9, there was NSLayoutAnchor that simplifies Auto Layout a lot. Auto Layout was declarative, but a bit verbose, now it is simpler than ever with an anchoring system.

NSLayoutConstraint.activate([
    box.topAnchor.constraint(equalTo: view.topAnchor, constant: 50),
    box.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20),
    box.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20),
    box.heightAnchor.constraint(equalToConstant: 100)
])
open class NSLayoutXAxisAnchor : NSLayoutAnchor<NSLayoutXAxisAnchor>
open class NSLayoutDimension : NSLayoutAnchor<NSLayoutDimension>
box.topAnchor.constraint(equalTo: view.leftAnchor, constant: 50),

Ambiguous error message with NSLayoutAnchor

I’ve gone into a few cases where the error messages from NSLayoutAnchor don’t help. If we mistakenly connect topAnchor with centerXAnchor, which are not possible as they are from different axes.

NSLayoutConstraint.activate([
    box.topAnchor.constraint(equalTo: view.centerXAnchor, constant: 50)
])
Value of optional type 'UIView?' must be unwrapped to refer to member 'centerXAnchor' of wrapped base type 'UIView'
NSLayoutConstraint.activate([
    imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    imageView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8),
    imageView.heightAnchor.constraint(equalToConstant: view.heightAnchor, mult0.7),
    imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0)
])

Abstractions over Auto Layout

NSLayoutAnchor is getting more popular now but it is not without any flaws. Depending on personal taste, there might be some other forms of abstractions over Auto Layout, namely Cartography and SnapKit, which I ‘ve used and loved. Here are a few of my takes on those.

Cartography

Cartography is one of the most popular ways to do Auto Layout in iOS. It uses operators which makes constraints very clear:

constrain(button1, button2) { button1, button2 in
    button1.right == button2.left - 12
}
constrain(logoImnageView, view1, view2) { logoImageView, view1, view2 in
    logoImageView.with == 74
    view1.left == view2.left + 20
}
Constraint.on(
  logoImageView.widthAnchor.constraint(equalToConstant: 74),
  view1.leftAnchor.constraint(equalTo: view2.leftAnchor, constant: 20),
)

SnapKit

SnapKit, originally Masonry, is perhaps the most popular Auto Layout wrapper

box.snp.makeConstraints { (make) -> Void in
   make.width.height.equalTo(50)
   make.center.equalTo(self.view)
}
https://github.com/onmyway133/blog/issues/22
keyB.snp.makeConstraints { (make) -> Void in
   make.left.equalTo(self.keyA.right)
}keyC.snp.makeConstraints { (make) -> Void in
   make.left.equalTo(self.keyB.right)
}keyD.snp.makeConstraints { (make) -> Void in
   make.left.equalTo(self.keyC.right)
}

The many overloading functions

There are also attempts to build simple Auto Layout wrapper functions but that escalates very quickly.

box.pinEdgesToSuperview()
box.pinEdgesToView(_ view: UIView)
box.pinEdgesToView(_ view: UIView, insets: UIEdgeInsets)
box.pinEdgesToView(_ view: UIView, insets: UIEdgeInsets, exclude: NSLayoutConstraint.Attribute)
box.pinEdgesToView(_ view: UIView, insets: UIEdgeInsets, exclude: NSLayoutConstraint.Attribute, priority: NSLayoutConstraint.Priority)

Embracing Auto Layout

All the Auto Layout frameworks out there are just convenient ways to build NSLayoutConstraint, in fact, these are what you normally need

  • Set translatesAutoresizingMaskIntoConstraints = false
  • Set isActive = true to enable constraints
NSLayoutConstraint.on([
    box.pinCenter(view: view),
    box.pin(size: CGSize(width: 100, height: 50))
])
let topConstraint = box.topAnchor.constraint(equalTo: view.topAnchor, constant: 50)
topConstraint.priority = UILayoutPriority.defaultLowNSLayoutConstraint.on([
    topConstraint
])

Making Auto Layout more convenient with the builder pattern

The above extension on NSLayoutConstraint works well. However, if you’re like me who wants even more declarative and fast Auto Layout code, we can use the builder pattern to make Auto Layout even nicer. The builder pattern can be applied to many parts of the code but I find it very well suited for Auto Layout. The final code is Anchors on GitHub, and I will detail how to make it.

activate(
  boxA.anchor.top.left,
  boxB.anchor.top.right,
  boxC.anchor.bottom.left,
  boxD.anchor.bottom.right
)

Which objects can interact with Auto Layout?

Currently, there are 3 types of objects that can interact withAuto Layout

  • UILayoutSupport, from iOS 7, for UIViewController to get bottomLayoutGuide and topLayoutGuide . In iOS 11, we should use safeAreaLayoutGuide from UIView instead
  • UILayoutGuide: using invisible UIView to do Auto Layout is expensive, that’s why Apple introduced layout guides in iOS 9 to help.
public class Anchor: ConstraintProducer {
  let item: AnyObject/// Init with View
  convenience init(view: View) {
    self.init(item: view)
  }/// Init with Layout Guide
  convenience init(layoutGuide: LayoutGuide) {
    self.init(item: layoutGuide)
  }// Init with Item
  public init(item: AnyObject) {
    self.item = item
  }
}
public extension View {
  var anchor: Anchor {
    return Anchor(view: self)
  }
}public extension LayoutGuide {
  var anchor: Anchor {
    return Anchor(layoutGuide: self)
  }
}

Which properties are needed in a layout constraint?

The builder patterns make building things declaratively by holding temporary values. Besides from, to, priority, identifier, we need an array of pins to handle cases where there are multiple created constraints. A center constraint results in both centerX and centerY constraints, and an edge constraint results in top, left, bottom and right constraints.

func paddingHorizontally(_ value: CGFloat) -> Anchor {
  removeIfAny(.leading)
  removeIfAny(.trailing
  pins.append(Pin(.leading, constant: value))
  pins.append(Pin(.trailing, constant: -value)
  return self
}

Inferring constraints

There are times we want to infer constraints, like if we want a view’s height to double its width. Since we already have width, declaring ratio should pair the height to the width.

box.anchor.width.constant(10)
box.anchor.height.ratio(2) // height==width*2
if sourceAnchor.exists(.width) {
  return Anchor(item: sourceAnchor.item).width
    .equal
    .to(Anchor(item: sourceAnchor.item).height)
    .multiplier(ratio).constraints()
} else if sourceAnchor.exists(.height) {
  return Anchor(item: sourceAnchor.item).height
    .equal
    .to(Anchor(item: sourceAnchor.item).width)
    .multiplier(ratio).constraints()
} else {
  return []
}

Retrieving a constraint

I see we’re used to storing constraint property in order to change its constant later. The constraints property in UIView has enough info and it is the source of truth, so retrieving constraint from that is more preferable.

boxA.anchor.find(.height)?.constant = 100// later
boxB.anchor.find(.height)?.constant = 100// later
boxC.anchor.find(.height)?.constant = 100

How to reset constraints

One of the patterns I see all over is resetting constraints in UITableViewCell or UICollectionViewCell. Depending on the state, the cell removes certain constraints and add new constraints. Cartography does this well by using group.

constrain(view, replace: group) { view in
    view.top  == view.superview!.top
    view.left == view.superview!.left
}
let g1 = group(box.anchor.top.left)
let g2 = group(box.anchor.top.right)
let g3 = group(box.anchor.bottom.right)
let g4 = group(box.anchor.bottom.left)

Where to go from here

In this article, we step a bit in time into manual layout, autoresizing masks and then to the modern Auto Layout. The Auto Layout APIs have improvements over the years and are recommended way to do layout. Learning declarative layout also helps me a lot when I learn Constraint Layout in Android, flexbox in React Native or the widget layout in Flutter.

https://github.com/onmyway133/Anchors
activate(
  lineBlock.anchor.left.bottom
)// later
activate(
  firstSquareBlock.anchor.left.equal.to(lineBlock.anchor.right),
  firstSquareBlock.anchor.bottom
)// later
activate(
  secondSquareBlock.anchor.right.bottom
)

Flawless iOS

🍏 Community around iOS development, mobile design, and marketing

Khoa Pham

Written by

Khoa Pham

Cageball apprentice at @hyperoslo. My apps https://onmyway133.github.io/

Flawless iOS

🍏 Community around iOS development, mobile design, and marketing