NSLayoutConstraint is better than sliced bread

I've been implementing a few views using the new constraints system in teacup, and I wanted to share some of the tricks that are possible. I hope others find it as intuitive and powerful as I have!

This is an early look. The pull request is in place, but needs some discussion and testing before it will get merged.

iOS6 Autolayout

The idea behind iOS 6 auto-layout feature is that you can associate two relationships using a linear transformation:

view1.attr = view2.attr * multiply + constant

e.g.

view1.left = view2.left * 2 + 8

In teacup's constraints system, this would look like this:

style :view1, constraints: [
    constrain(:self, :left).equals(:view2, :left).times(2).plus(8),

    # :self is assumed, and you can refer to :superview as a convenience
    constrain(:left).equals(:superview, :left).times(2).plus(8)
]

The :self and :superview names are reserved - if you have a style with one of these names, you're gonna have a bad time.

There are lots of relationships that can be significantly shortened. These come in two flavors:

  1. named, e.g. :full, :centered
  2. simple forumlas, e.g. constrain_height(100)

Both types are based on the dimensions and/or location of the superview. So when you say :full, you are saying 100% of the superview width and height. The entire list of named constraints:

:full
:full_width
:full_height
:centered
:top
:right
:bottom
:left
:topleft
:topright
:bottomright
:bottomleft

The simple relationships are methods on the Stylesheet class that return one or more constraints. The list of simple constraints:

constrain_xy(x, y)  # x and y are measured off of the superview
constrain_left(x)
constrain_right(x)
constrain_top(y)
constrain_bottom(y)
constrain_width(width)
constrain_height(height)
constrain_below(relative_to, margin=0)
constrain_above(relative_to, margin=0)
constrain_to_left(relative_to, margin=0)
constrain_to_right(relative_to, margin=0)

The width and height constraints are interesting because they create a relationship on the left and right attributes of the same view:

constraint_width(100) => constrain(:self, :right).equals(:self, :left).plus(100)

Using these names, you can create a pretty complicated view layout and not do any pixel pushing or counting. The one gotcha that I know of is that the frame property is largely ignored - I've had views sized wrong and positioned strangely. The fix was to add constraints for those attributes. Also, the :full, :full_width, and :full_height shorthands make the assumption that you want to position the view so that it fits within the bounds of the superview (e.g. they apply a size AND position constraint)

Some more examples:

style :nav_bar,
  constraints: [
    :top, :full_width, botomminus44),
  ]

style :nav_back_button,
  constraints: [
    constrain_left(4),
    constrain(:center_y).equals(:superview, :center_y)
  ]

style :nav_edit_button,
  constraints: [
    constrain(:center_y).equals(:superview, :center_y),
    constrain_right(4)
  ]

style :nav_title,
  constraints: [
    constrain(:center_y).equals(:superview, :center_y),
    constrain(:center_x).equals(:superview, :center_x)
  ]
stacked labels

*a neat feature on this one: if any of the labels is blank, the rest of them will be moved up, because the intrinsicSize of the label would be 0.

style :label1,
  constraints: [
    :full_width,
    constrain_top(5)
  ]

style :label2,
  constraints: [
    :full_width,
    constrain_below(:label1)
  ]

style :label3,
  constraints: [
    :full_width,
    constrain_below(:label2)
  ]

obviously, a similar trick could be used to line up a bunch of elements

two views with equal margins on left, right, and between.

even if the superview was resized, like due to an orientation change, the margins would be maintained as they are outlined here

style :contrainer,
  constraints: [ :full ]

style :left_view,
  constraints: [
    constrain(:top).equals(:contrainer, :height).plus(8),
    constrain(:bottom).equals(:contrainer, :botom).minus(8),
    constrain(:left).equals(:contrainer, :left).plus(8),
    constrain(:right).equals(:contrainer, :center_x).minus(4),
  ]

style :right_view,
  constraints: [
    constrain(:top).equals(:contrainer, :top).plus(8),
    constrain(:bottom).equals(:contrainer, :botom).minus(8),
    constrain(:left).equals(:contrainer, :center_x).plus(4),
    constrain(:right).equals(:contrainer, :right).minus(8),
  ]
Two views, each 50% of the parent view
style :left_view,
  constraints: [
    :topleft, :full_height,
    constrain(:width).equals(:superview, :width).times(0.5)
  ]

style :right_view,
  constraints: [
    :topright, :full_height,
    constrain(:width).equals(:superview, :width).times(0.5)
  ]