EasyAnchor

⚓️ Declarative, extensible, powerful Auto Layout

458
28
Swift

EasyAnchor

❤️ Support my apps https://indiegoodies.com/ ❤️

❤️❤️😇😍🤘❤️❤️

Version
Carthage Compatible
License
Platform
Swift

Table of contents

Story

I like to build view in code, so Auto Layout is my definite choice. The syntax has improved over the years, but I always want to do it with minimum effort. More repetitive code makes you tend to do copy paste and produce more bugs.

Read more How to make Auto Layout more convenient in iOS

Auto Layout APIs history

How new APIs were introduced over the years, so you know to set your deployment target

  • NSLayoutConstraint since iOS 6, macOS 10.7
  • isActive since iOS 8, macOS 10.10
  • NSLayoutAnchor, UI|NSLayoutGuide since iOS 9, macOS 10.11

Do you need another Auto Layout framework?

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

  • Call addSubview so that view is in hierarchy
  • Set translatesAutoresizingMaskIntoConstraints = false
  • Set isActive = true to enable constraints

Most of the time, NSLayoutAnchor is what you need. But if you need more, EasyAnchor can be your choice.

Examples

Tetris

Well, you can use Auto Layout to make Tetris. Auto Layout plays well with affine transform too. See code




activate(
  lineBlock.anchor.left.bottom
)

// later
activate(
  firstSquareBlock.anchor.left.equal.to(lineBlock.anchor.right),
  firstSquareBlock.anchor.bottom
)

// later
activate(
  secondSquareBlock.anchor.right.bottom
)

Piano

This is how to make a piano using apply and fixed spacing. See code

activate(
  c.anchor.left,
  b.anchor.right,
  c.anchor.top.bottom,
  c.anchor.top.bottom.width.apply(to: [d, e, f, g, a, b]),
  c.anchor.fixedSpacingHorizontally(togetherWith: [d, e, f, g, a, b], spacing: 0)
)

activate(
  cd.anchor.top,
  cd.anchor.size.equal.to(c.anchor.size).multiplier(2/3),
  cd.anchor.top.size.apply(to: [de, fg, ga, ab]),
  cd.anchor.centerX.equal.to(c.anchor.right),
  de.anchor.centerX.equal.to(d.anchor.right),
  fg.anchor.centerX.equal.to(f.anchor.right),
  ga.anchor.centerX.equal.to(g.anchor.right),
  ab.anchor.centerX.equal.to(a.anchor.right)
)

More

More example can be found in Example

Features

  • [x] Fluent builder syntax
  • [x] Easy to customize with protocol based
  • [x] Support iOS, macOS
  • [x] Support LayoutGuide
  • [x] Update and reset constraints
  • [x] Find existing constraints
  • [ ] Debug constraints
  • [ ] Visualize constraints

Basic with Anchor

Access Anchor

Prefer composition over extension, this is how you access anchor. Support View, LayoutGuide, LayoutSupport

let view = UIView()
view.anchor.left.right

let guide = UILayoutGuide()
guide.anchor.width.height

topLayoutGuide.anchor.bottom
bottomLayoutGuide.anchor.top

Activate constraints

Use activate which accepts variadic parameters. This is important, no matter how you create constraints, they don’t have any effect until you activate it ❗❗❗❗❗

activate(
  a.anchor.top.left,
  b.anchor.top.right,
  c.anchor.bottom.left,
  d.anchor.bottom.right
)

Attributes

Supports all attributes you can think of

anchor.top.left.bottom.right
  .leading.trailing
  .topMargin.bottomMargin.leftMargin.rightMargin
  .centerX.centerY
  .centerXWithinMargins.centerXWithinMargins
  .lastBaseline.firstBaseline
  .width.height

Relation

a.anchor.top.equal.to(b.bottom)
a.anchor.width.greaterThanOrEqual.to(b.anchor.height)
a.anchor.width.lessThanOrEqual.to(b.anchor)

Configuration

This is how to apply constant, multiplier, priority, identifier

a.anchor.top.equal.to(b.anchor.bottom)
  .constant(10).multiplier(1.5).priority(999)
  .id("verticalSpacingBetweenA-B")

Reference

Get references to constraints to modify it later on. In the ref closure, we get access to all the created constraints

var constraint: NSLayoutConstraint?
activate(
  view.anchor.center.constant(10).ref({ constraint = $0.first })
)

Convenient attributes

Use convenient attributes which combine multiple inner attributes

a.anchor.center  // centerX, centerY
a.anchor.size  // width, height
a.anchor.edges  // top, right, bottom, left

Convenient methods

Insets

a.anchor.edges.insets(EdgeInsets(top: 1, left: 2, bottom: 3, right: 4))  // top+1, left+2, bottom+3, right+4
a.anchor.edges.insets(5)  // top+5, left+5, bottom-5, right-5

Padding

a.anchor.paddingHorizontally(20) // leading+20, trailing-20
b.anchor.paddingVertically(20)	// top+20, bottom-20

Size

Size to another anchor

a.anchor.height.equal.to(b.anchor.width)
c.anchor.size.equal.to(d.anchor)

Size to a constant

a.anchor.height.equal.to(20)  // height==20
b.anchor.size.equal.to(20)  // width==20, height==20

You can’t just use constant because EasyAnchor will infer to the superview

c.anchor.width.constant(20)  // width==superview.width+20

Ratio

a.anchor.height.equal.to(a.anchor.width)  // height==width

Alternatively, you can just use ratio

a.anchor.width.constant(10)
a.anchor.height.ratio(2) // height==width*2
a.anchor.height.constant(10)
a.anchor.width.ratio(2) // width==height*2

Inference

You know what you mostly want to do. So does EasyAnchor 🎉. It does its best to infer so don’t have to write “obvious” code

Most of the time, you want to constraint to the superview

a.anchor.top.left // a.top == a.superview.top, a.left == a.superview.left

Most of the time, you want to constraint to the same attributes

a.anchor.top.bottom.width.equal.to(b.anchor) // a.top == b.top, a.bottom == b.bottom, a.width == b.width

Find existing constraints

You don’t need to declare variables to store constraints, you can just retrieve them back

a.anchor.find(.height)?.constant = 100

// later
b.anchor.find(.height)?.constant = 100

// later
c.anchor.find(.height)?.constant = 100

Animation

Animation is simple. You just change your constraint 's isActive or constant properties, then layoutIfNeeded in an animation block. You can use UIView.animate or UIViewPropertyAnimator

// Change constraint
a.anchor.find(.height)?.constant = 100
loginButtonHeightConstraint.isActive = false

let animator = UIViewPropertyAnimator(duration: 1, dampingRatio: 0.7)
animator.addAnimations { [weak self] in
  self?.view.layoutIfNeeded()
}

animator.startAnimation(afterDelay: 1)

Update constraints with Group

activate is just a convenient way to produce group, then set isActive on the Group. If you just want to group a set of constraints, then set isActive later on, use function group

In this example, we have 4 groups, then take turn to toggle which group gets activated

func toggle(_ group: Group) {
  [g1, g2, g3, g4].forEach { g in
    guard let g = g else {
      return
    }

    g.isActive = (g == group)
  }
}

g1 = group(a.anchor.top.left)
g2 = group(a.anchor.top.right)
g3 = group(a.anchor.bottom.right)
g4 = group(a.anchor.bottom.left)

g1.isActive = true

animator = Animator(view: self, animations: [
  {
    self.toggle(self.g2)
  },
  {
    self.toggle(self.g3)
  },
  {
    self.toggle(self.g4)
  },
  {
    self.toggle(self.g1)
  }
  ])

animator.start()

Extensible with ConstraintProducer

Group is a set of NSLayoutConstraint, which are produced by ConstraintProducer

public protocol ConstraintProducer {
  func constraints() -> [NSLayoutConstraint]
}

For now, there is Anchor and Builder which conforms to ConstraintProducer, you can extend EasyAnchor easily by conform to ConstraintProducer. For example

// This accepts a list of views, and build constraints
class MyAwesomeLayout: ConstraintProducer {
  init(views: [UIView]) {
    // Your code goes here
  }

  func constraints() -> [NSLayoutConstraint] {
    // Your code goes here
    return []
  }
}

let awesomeLayout = MyAwesomeLayout(views: [view1, view2])
activate(awesomeLayout)

Build quickly with Builder

Well, Anchor is for making constraints between 2 views. If you want to make constraints for multiple views at once, you can use multiple Anchor. There are some tasks that you do often, let Builder help you. These method below use Builder under the hood

EasyAnchor has a set of builders to help you avoid repetitive tasks and build UIs quickly 😎

Apply

Apply the same anchor to other views

a.anchor.left.height.apply(to: [b, c]),

Paging

Build a paging scrollView horizontally

addSubview(scrollView)
[a, b, c, d].forEach {
  scrollView.addSubview($0)
}

activate(
  scrollView.anchor.edges.insets(8),
  a.anchor.pagingHorizontally(togetherWith: [b, c, d], in: scrollView)
)

Fixed spacing

Add fixed spacing. The views will resize

activate(
  container.anchor.edges.insets(8),
  a.anchor.left.top.bottom,
  c.anchor.right,
  a.anchor.top.bottom.width.apply(to: [b, c]),
  a.anchor.fixedSpacingHorizontally(togetherWith: [b, c], spacing: 50)
)

Dynamic spacing

Add dynamic spacing using LayoutGuide. The spacing will resize

activate(
  container.anchor.edges.insets(8),
  a.anchor.size.equal.to(30),
  b.anchor.size.equal.to(30),
  c.anchor.size.equal.to(30),
  a.anchor.left.centerY,
  a.anchor.centerY.apply(to: [b, c]),
  c.anchor.right,
  a.anchor.dynamicSpacingHorizontally(togetherWith: [b, c])
)

Debug Auto Layout

Support multiple screen sizes

Installation

EasyAnchor is available through CocoaPods. To install
it, simply add the following line to your Podfile:

pod 'EasyAnchor'

EasyAnchor is also available through Carthage.
To install just write into your Cartfile:

github "onmyway133/EasyAnchor"

EasyAnchor can also be installed manually. Just download and drop Sources folders in your project.

Author

Khoa Pham, [email protected]

Contributing

We would love you to contribute to EasyAnchor, check the CONTRIBUTING file for more info.

License

EasyAnchor is available under the MIT license. See the LICENSE file for more info.