Mortar

Swift 5.0 Swift 4.2 Swift 4.0

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 之前的版本更新?阅读这里!

默认 Mortar 约束优先级在 v1.1 中发生了更改。 请阅读 README_DEFAULTS.md 文件以获取更多信息。

为什么?

是的,有很多 Auto Layout DSL 可供选择。 创建 Mortar 是为了弥补其他产品中感知到的弱点。

安装

Swift Package Manager (推荐)

将 Mortar 添加到你的 Package.swift 依赖项列表中

.package(url: "https://github.com/jmfieldman/Mortar.git", from: "2.0.0")

Cocoapods

注意:Cocoapods 支持已弃用;最后支持的 Mortar 版本是 1.7.0

你可以通过将其添加到你的 CocoaPods Podfile 来安装 Mortar

pod 'Mortar'

如果你想使用 Mortar VFL 语言

pod 'Mortar/MortarVFL'

或者你可以使用多种方法将此项目中的 Mortar.framework 文件包含到你自己的项目中。

Swift 版本支持

使用 Swift Package Manager 的 2.0.0 版本已使用 Xcode 14.2 和 Swift 5.7 进行了测试。

此 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 (Cocoapods,旧版 Swift)

Mortar 的默认实现声明了一个 MortarCreatable 协议 (create),这在旧版本的 swift 中会导致不公开默认 init() 方法的类出现问题。

如果你面向非常旧版本的 Swift,你可以使用

pod 'Mortar/Core_NoCreatable'
pod 'Mortar/MortarVFL_NoCreatable'

用法

Mortar 不需要任何类型的 closures。 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 支持所有标准布局属性

以及 iOS/tvOS 特定的属性

它还支持复合属性

隐式属性

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

使用 Layout Guides

在 iOS 上,你可以访问 UIViewController 的 layout guides。 来自 viewDidLoad() 内部的一个示例,该示例将视图放置在安全顶部下方。

// Super useful when trying to position views inside a navigation/tab controller!
view1.m_top   |<| self.m_safeTop

还有一个新的 UIViewController 属性 m_safeRegion,可帮助将视图与控制器视图的安全区域对齐。要使用此属性,你必须安装 MortarVFL 扩展。

// Center a view inside the safe region of a UIViewController that is a child of
// a navigation controller or tab controller
textField.m_center |=| self.m_safeRegion

使用 m_safeRegion 将创建一个“ghost”视图作为控制器根视图的子视图。 此 ghost 视图是隐藏的且不可交互的,仅用于定位。 它的类名称是 _MortarVFLGhostView,以防你在视图调试器中看到它。

乘数和常量

Auto Layout 约束可以应用乘数和常量。 这是使用正常的算术运算符完成的。 当使用算术运算符时,必须在右侧显式声明属性。

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]

优先级

你可以使用 ! 运算符为约束分配优先级。 有效的优先级为

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 方法,如果你没有正确平衡你的 pushes 和 pops,Mortar 将引发异常。

更改优先级

你可以通过调用 changePriority 方法来更改 MortarConstraintMortarGroup 的优先级。 这需要一个 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,它包装了几个与多亲和性属性(如 m_frame (4) 或 m_size (2))相关的 NSLayoutConstraint 实例。

你可以捕获 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)

抗压缩性和内容吸附性

Mortar 提供了一些简写属性来调整视图的抗压缩性和内容吸附优先级

// 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 语言,该语言大致等同于 Apple 自己的 Auto Layout VFL langauge。 主要优点是

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 使用 create 类函数扩展了 NSObject。 此类函数执行类的无参数实例化,并将新实例传递到提供的闭包中。 这允许您在创建时配置实例,这对于分离视图配置非常有用。

正如您在下面的示例中看到的,视图的配置与将其附加到视图控制器层次结构和布局所需的代码是分开的。

class MyController: UIViewController {

    // Instantiation/configuration
    let myLabel = UILabel.create {
        $0.text          = "Some Text"
        $0.textAlignment = .center
        $0.textColor     = .red
    }

    // UIViews can use the immediate init block
    let myButton = UIButton {
        $0.setTitle("Hello", for: .normal)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Hierarchy
        self.view |+| [
            myLabel,
            myButton
        ]

        // Layout
        myLabel.m_top     |=| self.view
        myLabel.m_centerX |=| self.view
        myButton.m_width  |=| myLabel
    }
}

结合层次结构和布局

将层次结构和布局组合到一个函数(如 SwiftUI)中的一个历史问题是,UIKit 要求两个视图在激活约束之前位于同一视图层次结构中。

这阻止了您执行以下操作

view1 |+| [
    UILabel.create {
        // Crash here, because the newly created UILabel is not
        // a subview of view1 until *after* the top-level |+| is
        // executed.
        $0.m_width |=| view1
    }
]

从 2.0.0 版本开始,Mortar 了解如何在创建层次结构时延迟约束激活。

view1 |+| [
    UILabel.create {
        // This is OK in Mortar 2.0.0; the constraint will not
        // activate until after the outer-most |+| executes.
        $0.m_width |=| view1
    }
]

也就是说,任何时候 |+||^| 运算符正在进行中,或者您使用新的 .addSubviews.addArrangedSubviews 结果构建器时,Mortar 知道不要激活任何约束分配,直到最外层层次结构分配完成后。

Mortar 2.0.0 现在还提供结果构建器作为添加子视图运算符的右侧。 结果构建器将左侧视图作为块参数,这让子视图可以轻松引用匿名父视图。

view |+| { view in
    UIStackView {
        $0.spacing = 1
        $0.axis = .vertical
        $0.m_width |=| view
    } |+| { stack in
        UILabel {
            $0.text = viewModel.helloText
            $0.m_height |=| stack
        }
        UILabel {
            $0.text = "World"
            $0.m_height |=| stack
            $0.reactive.text <~ viewModel.worldText
        }
        UIButton {
            $0.reactive.pressed = viewModel.pressed
        }
    }
]

将此与 ReactiveSwift 等反应式框架相结合,即使对于可以正确利用堆栈视图的大型自定义布局,也可以几乎完全实现单表达式视图定义。