Mortar allows you to create Auto Layout constraints using concise, simple code statements.
Use this:
view1.m_right |=| view2.m_left - 12.0
Instead of:
addConstraint(NSLayoutConstraint(
item: view1,
attribute: .Right,
relatedBy: .Equal,
toItem: view2,
attribute: .Left,
multiplier: 1.0,
constant: -12.0
))
Other examples:
/* Set the size of three views at once */
[view1, view2, view3].m_size |=| (100, 200)
/* Pin a 200px high, same-width view at the bottom of a container */
[view.m_sides, view.m_bottom, view.m_height] |=| [container, container, 200]
Yes, there are many Auto Layout DSLs to choose from. Mortar was created to fill perceived weaknesses in other offerings:
-
Mortar does not use blocks/closures like SnapKit or Cartography. These are distracting and ugly when coding constraints for controllers with many views.
-
Mortar is an attempt to get away from chaining methods like SnapKit does. Chained methods are good for providing semantic meaning to the line of code, but they are hard to parse quickly when you come back to your constraint section later.
-
Mortar supports multi-constraint macro properties (like
m_edges
,m_frame
,m_size
, etc). SwiftAutoLayout and other operator-based DSLs don't seem to have support for these in a concise form. Properties are prefixed with m_ to reduce potential for conflict with other View extensions. -
Mortar supports implicit property matching (other frameworks require declared properties on both sides of the statement.)
-
Mortar supports implicit tuple processing (you don't need to call out a tuple as a specific element like
CGRect
orCGSize
). -
Mortar supports multi-view alignment/constraints in a single line.
-
Additional goodies, like the
|+|
operator to visually construct view hierarchies rather than using tons of sequential calls toaddSubview()
.
You can install Mortar by adding it to your CocoaPods Podfile
:
pod 'Mortar'
Or you can use a variety of ways to include the Mortar.framework
file from this project into your own.
All versions of Mortar up to 0.10.4 were designed for Swift 2.2. This is the last release planned for Swift 2.2. You can force the usage of this version by specifying:
pod 'Mortar', '~> 0.10'
Version 0.11.1 is the release for Swift 2.3 compiled against the Xcode8 GM seed. Any bugfixes will be released in the 0.11.x version family. 0.11.x is the only version family that will support Swift 2.3. You can force usage of this version family by specifying:
pod 'Mortar', '~> 0.11'
Version 1.0 will be the first release of Mortar that is compatible with Swift 3.0.
Note that 1.0 is not available at the time of writing this README.
Mortar does not require closures of any kind. The Mortar operators (|=|
, |>|
and |<|
) instantiate and return constraints that are activated by default.
Mortar will set translatesAutoresizingMaskIntoConstraints
to false
for every view declared on the left side of an operator.
There are three mortar operators:
view1.m_width |=| 40 // Equal
view1.m_width |>| view2.m_width // Greater than or equal
view1.m_size |<| (100, 100) // Less than or equal
Mortar supports all of the standard layout attributes:
m_left
m_right
m_top
m_bottom
m_leading
m_trailing
m_width
m_height
m_centerX
m_centerY
m_baseline
And iOS/tvOS spceific attributes:
m_firstBaseline
m_leftMargin
m_rightMargin
m_topMargin
m_bottomMargin
m_leadingMargin
m_trailingMargin
m_centerXWithinMargin
m_centerYWithinMargin
It also supports composite attributes:
m_sides -- (left, right)
m_caps -- (top, bottom)
m_size -- (width, height)
m_center -- (centerX, centerY)
m_cornerTL -- (top, left)
m_cornerTR -- (top, right)
m_cornerBL -- (bottom, left)
m_cornerBR -- (bottom, right)
m_edges -- (top, left, bottom, right)
m_frame -- (top, left, width, height)
Mortar will do its best to infer implicit attributes!
The m_edges
attribute is implied when no attributes are declared on either side:
view1.m_edges |=| view2.m_edges // These two lines
view1 |=| view2 // are equivalent.
If an attribute is declared on one side, it is implied on the other:
view1.m_top |>| view2 // These two lines
view1 |>| view2.m_top // are equivalent.
You are required to put attributes on both sides if they are not the same:
view1.m_top |<| view2.m_bottom
On iOS you can use the layout guides of a UIViewController
. An example from inside viewDidLoad()
that puts a view just below the top layout guide:
view1.m_top |<| self.m_topLayoutGuideBottom
Auto Layout constraints can have multipliers and constants applied to them. This is done with normal arithmetic operators. Attributes must be explicitly declared on the right side When arthmetic operators are used.
view1.m_size |=| view2.m_size * 2 // Multiplier
view1.m_left |>| view2.m_right + 20 // Constant
view1 |<| view2.m_top * 1.4 + 20 // Both -- m_top is implied on the left
You can also set attributes directly to constants:
view1.m_width |=| 100 // Single-dimension constants
view1.m_size |=| 150.0 // Set multiple dimensions to the same constant
view1.m_size |>| (200, 50) // Set multiple dimensions to a tuple value
view1.m_frame |<| (0, 0, 50, 100) // Four-dimension tuples supported
Arithmetic can be done using tuples for multi-dimension attributes:
view1.m_size |=| view2.m_size + (50, 30)
view1.m_size |>| view2.m_size * (2, 3) + (10, 10)
The special inset operator ~
operates on multi-dimension attributes:
view1 |=| view2.m_edges ~ (20, 20, 20, 20) // view1 is inset by 20 points on each side
You are allowed to put attributes inside of tuples:
view1.m_size |=| (view2.m_width, 100)
Multiple constraints can be created using arrays:
view1 |=| [view2.m_top, view3.m_bottom, view4.m_size]
/* Is equivalent to: */
view1 |=| view2.m_top
view1 |=| view3.m_bottom
view1 |=| view4.m_size
[view1, view2, view3].m_size |=| (100, 200)
/* Is equivalent to: */
[view1.m_size, view2.m_size, view3.m_size] |=| (100, 200)
/* Is equivalent to: */
view1.m_size |=| (100, 200)
view2.m_size |=| (100, 200)
view3.m_size |=| (100, 200)
This might be a convenient way to align an array of views, for example:
[view1, view2, view3].m_centerY |=| otherView
[view1, view2, view3].m_height |=| otherView
If you put arrays on both sides of the constraint, it will only constrain elements at the same index. That is:
[view1.m_left, view2, view3] |=| [view4.m_right, view5, view6]
/* Is equivalent to: */
view1.m_left |=| view4.m_right
view2 |=| view5
view3 |=| view6
You can use this to create complex constraints on one line. For example, to create a 200-point high view that sits at the bottom of a container view:
[view.m_sides, view.m_bottom, view.m_height] |=| [container, container, 200]
You can assign priority to constraints using the !
operator. Valid priorities are:
.Low
,.Default
,.High
,.Required
- Any
UILayoutPriority
value
v0 |=| self.container.m_height
v1 |=| self.container.m_height ! .Low
v2 |=| self.container.m_height ! .Default
v3 |=| self.container.m_height ! .High
v4 |=| self.container.m_height ! .Required
v5 |=| self.container.m_height ! 300
You can also put priorities inside tuples or arrays:
view1 |=| [view2.m_caps ! .High, view2.m_sides ! .Low] // Inside array
view1.m_size |=| (view2.m_height ! .High, view2.m_width + 20 ! .Low) // Inside tuple
By default, constraints are given priority equal to 500 (out of 1000). Often times if you
may want large batches of constraints to have a different priority, and it is messy to include something like
! .Required
after every constraint.
You can change the global base default value by using setBase
:
MortarDefault.Priority.setBase(.Required)
You can use this in the AppDelegate
to change the app-wide default constraint priority.
Because this can only be changed on the main thread, it is safe to call just before your layout code. Keep in mind it will affect all future Mortar contraints! If you are adjusting the default for a single layout section, it is usually wiser to use the stack mechanism to change the default priority used in a frame of code:
MortarDefault.Priority.push(.Required)
v1 |=| v2 // Given priority .Required automatically
...
MortarDefault.Priority.pop()
You may only call the push/pop methods on the main thread, and Mortar will raise an exception if you do not properly balance your pushes and pops.
You can change the priority of a MortarConstraint
or MortarGroup
by calling the changePriority
method. This takes either a MortarLayoutPriority
enum, or a UILayoutPriority
value:
let c = view1 |=| view2 ! .Low // Creates 4 low-priority constraints (1 per edge)
c.changePriority(.High) // Sets all 4 constraints to high priority
Remember that you can't switch to or from Required
from any other priority level (this is an Auto Layout limitation.)
You can use the ~~
operator as a shorthand for constraint activation and deactivation. This makes the most sense as part of constraint declarations when you want to create initially-deactivated constraints:
let constraint = view1 |=| view2 ~~ .Deactivated
// Later on, it makes more semantic sense to call .activate():
constraint.activate()
// Even though this is functionally equivalent:
constraint ~~ .Activated
// It works with groups too:
let group = [
view1 |=| view2
view3 |=| view4
] ~~ .Deactivated
The basic building block is the MortarConstraint
, which wraps several NSLayoutConstraint
instances that are relevant to multi-affinity attributes like m_frame
(4) or m_size
(2).
You can capture a MortarConstraint
for later reference:
let constraint = view1.m_top |=| view2.m_bottom
The raw NSLayoutConstraint
elements can be accessed through the layoutConstraints
accessor:
let mortarConstraint = view1.m_top |=| view2.m_bottom
for rawLayoutConstraint in mortarConstraint.layoutConstraints {
...
}
You can create an entire group of constraints:
let group = [
view1.m_origin |=| view2,
view1.m_size |=| (100, 100)
]
Mortar includes a convenient typealias to refer to arrays of MortarConstraint
objects:
public typealias MortarGroup = [MortarConstraint]
You can now activate/deactivate constraints:
let constraint = view1.m_top |=| view2.m_bottom
constraint.activate()
constraint.deactivate()
let group = [
view1.m_origin |=| view2,
view1.m_size |=| (100, 100)
]
group.activate()
group.deactivate()
Constraints and groups have a replaceWith method that deactives the target and activates the parameter:
let constraint1 = view1.m_sides |=| view2
let constraint2 = view1.m_width |=| view2 ~~ .Deactivated
constraint1.replaceWith(constraint2)
let group1 = [
view1.m_sides |<| view2,
view1.m_caps |>| view2,
]
let group2 = [
view1.m_width |=| view2
] ~~ .Deactivated
group1.replaceWith(group2)
Mortar provides the |+|
and |^|
operators to quickly add a subview or array of subviews. This can be used to create visual expressions of the view hierarchy.
Now this:
self.view.addSubview(backgroundView)
self.view.addSubview(myCoolPanel)
myCoolPanel.addSubview(nameLabel)
myCoolPanel.addSubview(nameField)
Turns into:
self.view |+| [
backgroundView,
myCoolPanel |+| [
nameLabel,
nameField
]
]
Alternatively, if you want to see the upper subviews at the beginning of the array (so that visually, the views closer to the top of the file are closer to the user), use the |^|
operator:
self.view |^| [
myCoolPanel |^| [ // myCoolPanel is added second and
nameField, // is therefore on top of backgroundView
nameLabel
],
backgroundView
]