⌘ kean.blog

Introducing Align 2

  

Align, logo

This weekend, I decided to revisit one of my early frameworks - Align. I thought it had some good ideas behind it, but there was still a lot of room for improvement. I ended up re-engineering major parts of it, even taking some inspiration from SwiftUI. I’m thrilled with the results and I can’t wait to show it to you now.

Introduction #

The core idea is still the same: introducing a better, safer, more ergonomic version of Auto Layout anchors. Anchors are great, but they have some unfortunate design decisions:

  • translatesAutoresizingMaskIntoConstraints
  • You have to manually activate constraints
  • A bulky API
  • The API is still too low-level

The first three items are easy to solve. Align does it by introducing a custom Anchor type that works just as you would expect.

Anchor is a generic type. Align uses phantom types to parameterize it with an anchor type and an axis.

// Align two views along one of the edges
a.anchors.leading.equal(b.anchors.leading)

// Other options are available:
// a.anchors.leading.greaterThanOrEqual(b.anchors.leading)
// a.anchors.leading.greaterThanOrEqual(b.anchors.leading, constant: 10)

// Set height to the given value (CGFloat)
a.anchors.height.equal(30)

pin edges

Every view that you manipulate using Align has translatesAutoresizingMaskIntoConstraints automatically set to false so you no longer have to worry about it.

Align also automatically activates all of the created constraints. To activate all of the constraints at the same time, wrap the code that creates the constraints into Constraints {} closure. Internally, it will keep track of all of the constraints that you create, and when you are done, it will use NSLayoutConstraints.activate() to activate them all at the same time, which is faster than doing it one by one.

The last item – the API is still too low-level – is harder to address. What most frameworks end up doing is augmenting the meaning of the core equal, greaterThanOrEqual, lessThanOrEqual APIs. However, I found that I was almost immediately hitting some roadblocks with this design. For example, if you try to do the following, what does it really do? It is not clear from the API.

view.anchors.edges.greaterThanOrEqual(superview.anchors.edges)

// And this?
view.anchors.edges.lessThanOrEqual(superview.anchors.edges)

// From PureLayout, even without anchors
view.pinEdgesToSuperview(relation: .greaterThanOrEqual)

The other problem with this design is that if you try to generalize it, the implementation complexity grows dramatically. If you look at some of the other open source Auto Layout frameworks that follow this design, it’s hard to read and understand them.

I also quickly realized that this approach would hurt discoverability using Xcode code completion. And one of my primary goals was to make an API so clear that it would be easy to discover all of its features just using code completion. So this was a non-starter to me.

I scraped the idea of augmenting the existing methods and decided to split the API into two parts: Core and Semantic. Align APIs fit in the following quadrant.

Align, logo

The idea is to make Core API as dumb and as close to constraints as possible. Semantic API, on the other hand, is a high-level API that focuses on your goals rather than the constraints. This gives me freedom to choose method names and arguments that fit my needs.

Just to give you a taste of what these semantic APIs look like, here is an example of pin() which is by far my favorite API in Align.

view.anchors.edges.pin(insets: 20, alignment: .center)

pin edges with center alignment

Not bad, huh? Barely any code, six constraints created covering one of the common UI use cases. And this is just one example. Alignment parameter in pin() is extremely powerful. It has two components: vertical and horizontal. It covers a lot of possible layouts that you’ll might want to create.

There are multiple ways to learn Align APIs. The APIs surface is small and easily discoverable using Xcode code completion. The README consains an illustrated guide. There is even a cheat sheet available.

Align cheat sheet

Let me give you a brief tour of some other APIs in Align.

Anchors #

Core API #

You’ve already seen some of the Core APIs, but just to reiterate.

// Align two views along one of the edges
a.anchors.leading.equal(b.anchors.leading)

// Other options are available:
// a.anchors.leading.greaterThanOrEqual(b.anchors.leading)
// a.anchors.leading.greaterThanOrEqual(b.anchors.leading, constant: 10)

// Set height to the given value (CGFloat)
a.anchors.height.equal(30)

pin edges

Note. Every view that you manipulate using Align has translatesAutoresizingMaskIntoConstraints set to false. Align also automatically activates all of the created constraints.

Align has full test coverage. If you’d like to learn about which constraints (NSLayoutConstraint) Align creates each time you call one of its methods, test cases are a great place to start.

Align also allows you to offset and multiple anchors.

// Offset one of the anchors, creating a "virtual" anchor
b.anchors.leading.equal(a.anchors.trailing + 20)

// Set aspect ratio for a view
b.anchors.height.equal(a.anchors.width * 2)

pin edges

Semantic API #

// Set spacing between two views
a.anchors.bottom.spacing(20, to: b.anchors.top)

// Pin an edge to the superview
a.anchors.trailing.pin(inset: 20)

pin edges

Anchor Collections #

With Align, you can manipulate multiple edges at the same time, creating more than one constraint at a time.

Edges #

pin() is probably the most powerful and flexible API in Align.

view.anchors.edges.pin(insets: 20)

// Same as the following:
view.anchors.edges.pin(
    to: view.superview!
    insets: EdgeInsets(top: 20, left: 20, bottom: 20, trailing: 20),
    alignment: .fill
)

pin edges

By default, pin() method pin the edges to the superview of the current view. However, you can select any target view or layout guide:

// Pin to superview
view.anchors.edges.pin()

// Pin to layout margins guide
view.anchors.edges.pin(to: container.layoutMarginsGuide)

// Pin to safe area
view.anchors.edges.pin(to: container.safeAreaLayoutGuide)

Align also provides a convenient way to access anchors of the layout guide: view.anchors.safeArea.top.

By default, pin() users .fill alignment. There are variety of other alignments available.

view.anchors.edges.pin(insets: 20, alignment: .center)

pin edges with center alignment

You can create constraint along the given axis.

view.anchors.edges.pin(insets: 20, axis: .horizontal, alignment: .center)

pin edges with center alignment for horizontal axis

Or pin the view to to a corner.

view.anchors.edges.pin(insets: 20, alignment: .topLeading)

pin edges with center alignment for horizontal axis

You can create custom alignments (see Alignment type) by providing a vertical and horizontal component.

let alignment = Alignment(vertical: .center, horizontal: .leading) 
anchors.edges.pin(insets: 20, alignment: alignment)

pin edges with center alignment for horizontal axis

Center #

a.anchors.center.align()

size equal

Size #

a.anchors.size.equal(CGSize(width: 120, height: 40))

greaterThanEqual and lessThanOrEqual options are also available

size equal

a.anchors.size.equal(b)

size equal other view

Advanced Stuff #

By default, Align automatically activates created constraints. Using Constraints API, constraints are activated all of the same time when you exit from the closure. It gives you a chance to change priority of the constraints.

Constraints(for: title, subtitle) { title, subtitle in
    // Align one anchor with another
    subtitle.top.spacing(10, to: title.bottom + 10)

    // Manipulate dimensions
    title.width.equal(100)

    // Change a priority of constraints inside a group:
    subtitle.bottom.pin().priority = UILayoutPriority(999)
}

Constraints also give you an easy access to Align anchors (notice, there is no .anchors call in the example). And if you want Align to not activate constraints, there is an option for that:

Constraints(activate: false) {
    // Create your constraints here
}

Final Thoughts #

I hope you are as excited as I am about Align 2. It has a powerful, easy to learn, and easily discoverable API. To give it a try, please visit the Align repository.