Mortar 允许您使用简洁、简单的代码语句创建 Auto Layout 约束。
使用这个
view1.m_right |=| view2.m_left - 12.0
代替
addConstraint(NSLayoutConstraint(
item: view1,
attribute: .right,
relatedBy: .equal,
toItem: view2,
attribute: .left,
multiplier: 1.0,
constant: -12.0
))
其他示例
/* 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]
/* VFL syntax */
view1 |>> viewA || viewB[==44] | 20 | viewC[~~2]
从v1.1以前的版本升级?请阅读这个!
自v1.1版本起,默认Mortar约束优先级的更改已生效。请阅读README_DEFAULTS.md文件获取更多信息。
为什么?
是的,有很多Auto Layout DSL可以供选择。Mortar是为了填补其他提供产品中感知到的弱点而创建的。
-
Mortar不使用blocks/closures(如SnapKit或Cartography)。这在为具有许多视图的控制器编写约束时会产生干扰和丑陋的代码。
-
Mortar试图摆脱SnapKit那样的方法链。方法链对于提供代码行的语义意义很好,但当你后来回到约束段落时,它们难以快速解析。
-
Mortar支持多约束宏属性(如
m_edges
、m_frame
、m_size
等)。SwiftAutoLayout和其他基于操作符的DSL似乎不支持这些属性以简洁的形式。属性以m_为前缀以减少与其他View扩展的冲突。 -
Mortar支持隐式属性匹配(其他框架要求在语句两侧都声明属性。)
-
Mortar支持隐式元组处理(您不需要将元组作为一个特定元素像
CGRect
或CGSize
那样调出来。) -
Mortar支持在一行中支持多个视图的对齐/约束。
-
Mortar支持健壮的编译时VFL语法,该语法使用视图直接(而不是字典查找)。
-
其他好处,如使用
|+|
操作符可视地构建视图层次结构,而不是使用大量的连续调用`addSubview()`。
安装
您可以通过将 Mortar 添加到您的 CocoaPods Podfile
来进行安装
pod 'Mortar'
如果您想使用 Mortar VFL 语言
pod 'Mortar/MortarVFL'
或者您可以使用各种方法将此项目中的 Mortar.framework
文件包含到您的项目中。
Swift 版本支持
这个 README 反映了 Swift 3 Mortar 版本中使用的更新语法和常量。对于 Swift 2.x 文档,请参阅
README_SWIFT2.md
文件。
pod 'Mortar', '~> 1.6' # Swift 5.0
pod 'Mortar', '~> 1.5' # Swift 4.2
pod 'Mortar', '~> 1.4' # Swift 4.0
pod 'Mortar', '~> 1.3' # Swift 3.1
禁用 MortarCreatable
Mortar 的默认实现声明了一个 MortarCreatable 协议(m_create),在 Swift 的最新版本中,这会导致不暴露默认 init() 方法的类出现问题。
在下一个主要版本发布之前,您可以使用
pod 'Mortar/Core_NoCreatable'
pod 'Mortar/MortarVFL_NoCreatable'
来安装一个不将此协议附加到 NSObject 的 Mortar 版本。然后,您可以使用
extension your_class_name: MortarCreatable { }
使用方法
Mortar 不需要任何类型的闭包。Mortar 操作符(|=|
、|>|
和 |<|
)会实例化并返回默认激活的约束。
Mortar 会将操作符左边声明的每个视图的 translatesAutoresizingMaskIntoConstraints
设置为 false
。
相等、小于还是大于?
有三个 mortar 操作符
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 支持所有标准的布局属性
m_left
m_right
m_top
m_bottom
m_leading
m_trailing
m_width
m_height
m_centerX
m_centerY
m_baseline
以及 iOS/tvOS 特定属性
m_firstBaseline
m_leftMargin
m_rightMargin
m_topMargin
m_bottomMargin
m_leadingMargin
m_trailingMargin
m_centerXWithinMargin
m_centerYWithinMargin
它也支持组合属性
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 将尽最大努力推断隐式属性!
当在两边都没有声明属性时,m_edges
属性是隐式的
view1.m_edges |=| view2.m_edges // These two lines
view1 |=| view2 // are equivalent.
如果在一侧声明了属性,则在另一侧也是隐式的
view1.m_top |>| view2 // These two lines
view1 |>| view2.m_top // are equivalent.
如果它们不相同,则需要在两边都放置属性
view1.m_top |<| view2.m_bottom
使用布局指南
在 iOS 中,你可以访问 UIViewController
的布局指南。以下是一个例子,在 viewDidLoad()
中将视图放置在上边距布局指南下方
// Super useful when trying to position views inside a navigation/tab controller!
view1.m_top |<| self.m_topLayoutGuideBottom
还新增了一个 UIViewController
属性 m_visibleRegion
,以帮助对齐视图到控制器视图的上边距布局指南下方和下边距布局指南上方的区域。要使用此属性,必须已安装 MortarVFL 扩展。
// Center a view inside the visible region of a UIViewController that is a child of
// a navigation controller or tab controller
textField.m_center |=| self.m_visibleRegion
使用 m_visibleRegion
会在控制器的根视图创建一个“幽灵”视图作为子视图。这个幽灵视图是隐藏的且不可交互的,仅用于定位。其类名是 _MortarVFLGhostView
,以防你在视图调试器中看到它。
乘数和常量
自动布局约束可以应用乘数和常数。这是通过常规算术运算符完成的。当使用算术运算符时,必须在右侧显式声明属性。
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
您还可以直接将属性设置为常数。
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
可以使用元组进行多维度属性的算术运算。
view1.m_size |=| view2.m_size + (50, 30)
view1.m_size |>| view2.m_size * (2, 3) + (10, 10)
特殊内边距运算符 ~
操作多维度属性。
view1 |=| view2.m_edges ~ (20, 20, 20, 20) // view1 is inset by 20 points on each side
元组中的属性
您可以在元组中放置属性。
view1.m_size |=| (view2.m_width, 100)
多个同时约束
可以使用数组创建多个约束。
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)
这可能是有利于对视图数组进行对齐的便捷方式,例如。
[view1, view2, view3].m_centerY |=| otherView
[view1, view2, view3].m_height |=| otherView
当您在约束的两边都放置数组时,它只会约束相同索引的元素。也就是说
[view1.m_left, view2, view3] |=| [view4.m_right, view5, view6]
/* Is equivalent to: */
view1.m_left |=| view4.m_right
view2 |=| view5
view3 |=| view6
您可以使用它来在一行中创建复杂的约束。例如,创建高度为200点的视图,使其位于容器视图的底部
[view.m_sides, view.m_bottom, view.m_height] |=| [container, container, 200]
优先级
您可以使用!
运算符为约束分配优先级。有效优先级有:
.low
,.medium
,.high
,.required
- 任何
UILayoutPriority
值
v0 |=| self.container.m_height
v1 |=| self.container.m_height ! .low
v2 |=| self.container.m_height ! .medium
v3 |=| self.container.m_height ! .high
v4 |=| self.container.m_height ! .required
v5 |=| self.container.m_height ! 300
您也可以在元组或数组中放置优先级。
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
默认优先级
Mortar v1.1中默认值已更改;如果您正在更新,请参阅README_DEFAULTS.md。
默认情况下,约束的优先级为.required
,等于1000(除以1000),与Apple的约束方法使用的默认值相同。有时您可能希望大量约束有不同的优先级,并且在每个约束后包含诸如! .medium
之类的项很麻烦。
您可以使用set
更改全局基本默认值。
MortarDefault.priority.set(base: .medium)
您可以在AppDelegate
中使用此功能来更改应用级别的默认约束优先级。
因为这个操作只能在主线程上执行,所以建议在布局代码之前调用。请注意,这将影响所有未来的Mortar约束!如果您只想调整单个布局部分的默认值,通常更明智的做法是使用栈机制来更改代码框架中使用的默认优先级。
MortarDefault.priority.push(.low)
v1 |=| v2 // Given priority .low automatically
...
MortarDefault.priority.pop()
您只能在主线程上调用push/pop方法,如果您的push和pop操作不平衡,Mortar将引发异常。
更改优先级
通过调用changePriority方法,您可以使用code>changePriority更改一个code>MortarConstraint或MortarGroup
的优先级。这可以接受一个MortarLayoutPriority
枚举或一个UILayoutPriority
值。
let c = view1 |=| view2 ! .low // Creates 4 low-priority constraints (1 per edge)
c.changePriority(to: .high) // Sets all 4 constraints to high priority
请记住,您不能从任何其他优先级切换到Required
或从其中切换出来(这是Auto Layout的限制)。
创建禁用约束
您可以将运算符作为约束激活和禁用的一个简写。当您想在初始化时创建禁用约束时,这在与约束声明一起使用时最有意义。
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
保持约束引用
基本构建块是MortarConstraint
,它包装了几种与多亲和属性相关的NSLayoutConstraint
实例,如m_frame
(4)或m_size
(2)。
您可以捕获一个MortarConstraint
以供以后引用。
let constraint = view1.m_top |=| view2.m_bottom
原始的NSLayoutConstraint
元素可以通过layoutConstraints
访问器获取。
let mortarConstraint = view1.m_top |=| view2.m_bottom
for rawLayoutConstraint in mortarConstraint.layoutConstraints {
...
}
您现在可以激活/禁用约束。
let group = [
view1.m_origin |=| view2,
view1.m_size |=| (100, 100)
]
Mortar提供了一个方便的类型别名来引用MortarConstraint
对象的数组。
public typealias MortarGroup = [MortarConstraint]
现在可以激活/禁用约束。
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()
替换约束组
约束和组都有一个replace方法,可以禁用目标并激活参数。
let constraint1 = view1.m_sides |=| view2
let constraint2 = view1.m_width |=| view2 ~~ .deactivated
constraint1.replace(with: constraint2)
let group1 = [
view1.m_sides |<| view2,
view1.m_caps |>| view2,
]
let group2 = [
view1.m_width |=| view2
] ~~ .deactivated
group1.replace(with: group2)
压缩抵抗和内容拥抱
砂浆提供一些简写属性来调整视图的压缩抵抗和内容拥抱优先级
// Set both horizontal and vertical compression resistance priority simultaneously:
view1.m_compResist = 1
// Set horizontal and vertical compression resistance independently:
view1.m_compResistH = 300
view1.m_compResistV = 800
// Set both horizontal and vertical content hugging priority simultaneously:
view1.m_hugging = 1
// Set horizontal and vertical content hugging independently:
view1.m_huggingH = 300
view1.m_huggingV = 800
您可以独立获取水平和垂直值,但不能一起获取
// These getters are fine:
let c1 = view1.m_compResistH
let c2 = view1.m_compResistV
let h1 = view1.m_huggingH
let h2 = view1.m_huggingV
// These getters raise exceptions:
let cr = view1.m_compResist
let hg = view1.m_hugging
MortarVFL
Mortar 支持 VFL 语言的版本,与苹果自身的 Auto Layout VFL 语言大致相同。主要优点是:
- 您可以直接与现有的 Mortar 属性支持一起使用
- 视图被直接引用(而不是使用字典),以便于编译时检查
- 全量支持基于重量的相对尺寸
- 更加简洁:基于操作符而不是函数/字符串
由于 MortarVFL 严重依赖自定义操作符,因此它仅限于自己的扩展。这些操作符可能与您正在使用的其他库不兼容,因此我们不希望 Mortar 核心与之冲突。
pod 'Mortar/MortarVFL'
MortarVFL 内部组合
MortarVFL 语句的核心是一系列沿着水平或垂直轴依次定位的 VFL 节点。节点列表可能看起来像这样:
viewA | viewB[==viewA] || viewC[==40] | 30 | viewD[~~1] | ~~1 | viewE[~~2]
// viewA has a size determinde by its intrinsic content size
// viewA is separated from viewB by 0 points (| operator)
// viewB has a size equal to viewA
// viewB is separated from viewC by the default padding (8 points; || operator)
// viewC has a fixed size of 40
// viewC is separated from viewD by a space of 30 points
// viewD has a weighted size of 1
// viewD is separated fom viewE by a weighted space of 1
// viewE has a weighted size of 2
VFL 节点
- 表示空白、一个视图或多个视图
- 具有固定间距或加权间距
节点用 |
或 ||
操作符分隔。
|
操作符在节点之间引入零额外距离。您可以使用此操作符将节点直接相连,间距为零,或者在其中插入自己的固定/加权数值(例如 | 30 |
或 | ~~2 |
)。在这种情况下,30
和 ~~2
被视为表示空白(没有附加视图)的节点。
||
操作符将节点分隔为默认填充(8 个点)。
代表视图的节点尽可能遵守在现有约束和优先级条件下的视图的固有内容。视图节点也可以包含一个下标,为其提供尺寸约束。您可以使用 [==#]
为视图提供固定大小,或使用 [~~#]
为视图提供加权大小。您还可以引用其他视图,例如,使用 [==viewA]
为节点的视图提供与所引用视图相同的约束。
如果存在循环视图引用,MortarVFL 将引发错误,例如 viewA[==viewB] | viewB[==viewA]
节点中的数组
作为一个高级技术,您可以在一个节点中使用视图数组。它看起来可能像这样
viewA || [viewB, viewC, viewD][==40] || viewE
这使数组的节点彼此平行排列。在上面的例子中,viewB、viewC 和 viewD 都将具有 40 点的大小,并且将与 viewA 和 viewE 邻居。这对复杂的基于网格的布局非常有用。
捕获
MortarVFL语句至少需要由一个视图属性在某一端进行捕获。这些捕获看起来可能像这样
// viewB and viewC will take equal width between the
// right edge of viewA and the left edge of viewD
viewA.m_right |> viewB[~~1] | viewC[~~1] <| viewD.m_left
// viewB and viewC will be equal width between the
// left/right edges of viewA, inset by 8pt padding
// and separated by 40pts.
viewA ||>> viewB[~~1] | 40 | viewC[~~1]
MortarVFL支持与水平垂直间距类似的方式。水平操作符使用 >
字符,而垂直操作符使用 ^
字符。除此之外,它们的行为相似。例如,上述语句的垂直版本将是
// viewB and viewC will take equal height between the
// bottom edge of viewA and the top edge of viewD
viewA.m_bottom |^ viewB[~~1] | viewC[~~1] ^| viewD.m_top
Mortar 将确保您的操作符与您选择的属性兼容。例如,使用 |>
与 m_top
将导致轴不匹配并引发异常。
隐式捕获属性
如果您不提供捕获终端的属性,Mortar 将根据轴和位置派生它们
// These are equivalent:
viewA.m_left |> viewB | viewC <| viewD.m_right
viewA |> viewB | viewC <| viewD
// These are equivalent:
viewA.m_top |^ viewB | viewC ^| viewD.m_bottom
viewA |^ viewB | viewC ^| viewD
重要观察: 隐式属性可能与你预期的相反。这是因为隐式属性通常用于捕获位于父视图边界内的视图,因此我们使用外边缘,而不是内边缘。
隐式包围
如果您想要使 MortarVFL 节点在单个视图的边界之内,您可以使用包围操作符而不是在两个终端放置相同的视图。
包围操作符使用 >>
或 ^^
// viewB and viewc will be equal width between the
// left/right edges of viewA, inset by 8pt padding
// and separated by 40pts.
viewA ||>> viewB[~~1] | 40 | viewC[~~1]
// viewB will be twice as tall as viewC; both will be between
// the top/bottom edges of viewA.
viewA |^^ viewB[~~2] | viewC[~~1]
// Using m_visibleRegion is helpful for layouts in child view controllers
// to get views laid out inside the visible region, not under nav/tab bars
self.m_visibleRegion ||^^ viewA | viewB | viewC
单端语句
到目前为止,所有示例都展示了由两个属性(左右、上下)包围的语句。
对于两边都被包围的语句,您不能使用所有固定间距。这意味着您至少需要一个加权或固有尺寸的节点。这允许mortar在终端之间调整约束。如果您只有固有尺寸的节点,并且它们的压缩阻力和内容紧密度会强制为.required。
对于只有一个终端的语句,情况正好相反。 您不能使用任何基于权重的节点,并且它们都必须是固定大小或固有内容大小。这是因为没有第二个端点可以用来作为相对尺寸的锚点。
单端语句看起来与其他语句相同,但尾随操作符使用感叹号:!
不幸的是,这看起来非常像管道操作符,所以不要混淆。具体来说,当仅将一个语句附加到尾随属性时,请使用<!
、<!!
、^!
或^!!
。
// viewB will be placed at the right edge of viewA and be 44pts wide.
// viewC will be placed 8pts (padding) right of viewB and will be 88pts wide.
viewA.m_right |> viewB[==44] || viewC[==88]
// viewC will be placed at the left edge of viewA and be 88pts wide.
// viewB will be placed 8pts (padding) left of viewC and will be 44pts wide.
viewB[==44] || viewC[==88] <! viewA.m_left
// viewB will be placed at the bottom edge of viewA and be 44pts high.
// viewC will be placed 8pts below viewB and respect its intrinsic content height.
viewA.m_bottom |^ viewB[==44] || viewC
// viewC will be placed at the top edge of viewA and be 88pts high.
// viewB will be placed 8pts above viewC and will be 44pts high.
viewB[==44] || viewC[==88] ^! viewA.m_top
再次注意,使用感叹号!
作为尾随单端语句,并且没有任何基于权重的节点。前导单端语句使用带有管道的运算符:|>
示例
在Examples/MortarVFL项目中,有几个MortarVFL的示例。
视觉视图层次结构创建
Mortar提供了|+|
和|^|
运算符,可以快速添加子视图或子视图数组。这可以用于创建视图层次结构的视觉表达。
现在这
self.view.addSubview(backgroundView)
self.view.addSubview(myCoolPanel)
myCoolPanel.addSubview(nameLabel)
myCoolPanel.addSubview(nameField)
变为
self.view |+| [
backgroundView,
myCoolPanel |+| [
nameLabel,
nameField
]
]
或者,如果您想在数组的开头显示上级子视图(因此在视觉上,文件顶部的视图更接近用户),请使用|^|
运算符。
self.view |^| [
myCoolPanel |^| [ // myCoolPanel is added second and
nameField, // is therefore on top of backgroundView
nameLabel
],
backgroundView
]
初始化 NSObject 于创建时
Mortar 类通过 m_create
类函数扩展 NSObject
。此类函数执行无参数的类实例化,并将新建的实例传递到提供的闭包中。这允许你在创建时配置实例,这对于视图配置的模块化非常方便。
正如以下示例所示,视图的配置与将其附加到视图控制器层次结构和布局所需的代码是分离的。
class MyController: UIViewController {
// Instantiation/configuration
let myLabel = UILabel.m_create {
$0.text = "Some Text"
$0.textAlignment = .center
$0.textColor = .red
}
override func viewDidLoad() {
super.viewDidLoad()
// Hierarchy
self.view |+| [
myLabel
]
// Layout
myLabel.m_top |=| self.view
myLabel.m_centerX |=| self.view
}
}