GetLaid
GetLaid 是一个通过简洁易读的代码来布局复杂 UI 的精简框架。
为什么使用 GetLaid?
✅ 可读性- 语法接近自然语言,而非技术上复杂。
- 所有约束都采用
source.constrain(to: target)
的形式。 - 运算符
>>
可以提供进一步的澄清:source >> target
✅ 简洁性- 代码行非常短,且涉及较少的函数参数。
- 单行代码可以完成很多任务,通过结合目标,如
allButTop
和size
。
✅ 简单性/灵活性- 简单一致的系统设计:了解一个事物就可以做任何事情。
- 无缝覆盖父视图、安全区域和系统间距
✅ 易于高级布局- 使用
offset(CGFloat)
、min
、max
和at(_ factor: CGFloat)
修改任何约束目标。 - 链式修改目标:
item1 >> item2.size.at(0.5).min
- 使用
✅ 兼容性- 适用于 iOS、tvOS 和 macOS。
- 同样适用于视图、UILayoutGuide 和 NSLayoutGuide。
为什么不使用 Interface Builder?
嗯,那将是疯狂的事情。
为什么自动布局包装器?
在没有类似框架的情况下,编写程序化自动布局从未有难度。全是关于创建 NSLayoutConstraint
对象,它只有一个 强大的初始化器。
从 iOS/tvOS 9.0 和 macOS 10.11 开始,我们也有 NSLayoutAnchor
,它在 NSLayoutConstraint
之上添加了一个原生抽象层,进一步减少了对任何 AutoLayout 包装器的需求。
此时,自动布局包装器所能做的只是使布局代码在使用的点上更加有意义、可读和简洁。GetLaid 正是如此。
为什么不使用其他自动布局包装器?
现代自动布局包装器,如 SnapKit,几乎在简单的任务上过于聪明。
box.snp.makeConstraints { (make) -> Void in
make.width.height.equalTo(50)
make.center.equalTo(self.view)
}
经典自动布局包装器,如 PureLayout,语法更简单,但仍然很冗长。
box.autoSetDimensions(to: CGSize(width: 50, height: 50))
box.autoCenterInSuperView()
GetLaid 将 AutoLayout 精简到极致。只需读取运算符 >>
作为 "约束到"。
box >> 50
box >> box.parent?.center
所以,哪个更好看,uh?如果你能舍弃华丽,但欣赏可读性,GetLaid 可能适合你。
这里还有 一个更详细的比较,显示了与 GetLaid 的替代品相比的布局代码看起来如何。
安装
使用 Swift Package Manager,您只需通过 Xcode (11+) 添加 GetLaid 包。
或者您可以手动调整项目的 Package.swift 文件。
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.package(url: "https://github.com/flowtoolz/GetLaid.git",
.upToNextMajor(from: "3.0.0"))
],
targets: [
.target(name: "MyAppTarget",
dependencies: ["GetLaid"])
]
)
然后运行 $ swift build
或 $ swift run
。
target "MyAppTarget" do
pod "GetLaid", "~> 3.0"
end
然后运行 $ pod install
。
最后,在您的 Swift 文件中
import GetLaid
添加子视图和布局指南
通用函数addForAutoLayout
添加子视图并为其准备AutoLayout。它返回其作为精确类型的子视图。使用此函数添加子视图
class List: UIView {
// ... other code, including call to addSubviews() ...
func addSubviews() {
addForAutoLayout(header) >> allButBottom // add header to the top
}
private let header = Header()
}
如果你不使用addForAutoLayout
,请记住将结合到AutoLayout中的视图的translatesAutoresizingMaskIntoConstraints = false
设置。
还有一个辅助函数用于向视图添加新布局指南
let guide = view.addLayoutGuide()
约束一个位置
你总是对你要约束的确切事物调用constrain(to:)
。你总是可以用缩写操作符>>
来替换这个函数,我们将在示例中这么做。这些行是等价的
view1.top.constrain(to: view2.lastBaseline)
view1.top >> view2.lastBaseline
所有布局属性都可以这样使用,而基线在布局指南上不可用。
如果源和目标引用的是同一属性,你可以在一边省略该属性。这些是等价的
item1.left >> item2.left
item1.left >> item2
item1 >> item2.left
你可以修改约束目标,并且可以连续修改这些。
item1 >> item2.left.offset(8)
item1 >> item2.left.min // >= item2.left
item1 >> item2.left.max // <= item2.left
item1 >> item2.left.at(0.5) // at 0.5 of item2.left
item1 >> item2.left.min.offset(8)
约束多个位置
你可以一次约束多个位置
item1 >> item2.allButTop(leadingOffset: 5, // leading, bottom, trailing
bottomOffset: -5)
item1 >> item2.center // centerX, centerY
item1 >> item2.all // all edges
item1 >> item2 // shorthand for .all
可用的位置目标组合有
全部
除了顶部
除了leading
除了左边
除了底部
除了trailing
除了右边
中心
所有这些都需要偏移量作为参数,用于正好约束的位置,顺序是逆时针的。
约束一个尺寸
你可以像处理位置一样约束宽度和高度
item1.width >> item2.height
与位置一样,你可以省略冗余属性,修改目标,并且可以链式修改。
item1 >> item2.height.at(0.6).min // >= 60% of item2.height
你可以将尺寸约束为常量大小。这些是等价的
item.width >> .size(100)
item.width >> 100
省略尺寸以将两个尺寸都约束为相同的常量。这些是等价的
item >> .size(100) // square with edge length 100
item >> 100 // same
您可以对常量尺寸的目标进行修改,就像修改其他目标一样,针对一个或两个维度。不幸的是,为了与 >>
操作符一起使用,Swift 需要显式目标类型作为上下文。有两种解决方案,一种更本地化,另一种更美观。
item >> .size(100).max // WON'T COMPILE❗
item >> DimensionTarget.size(100).max // spells out the target type (ugly)
item >> layoutSize(100).max // global func makes the target (better)
旁注: 全局函数
layoutSize
不能简单地命名为size
,因为这从技术上会产生名称冲突,并且从意义上来说也不会正确建立 AutoLayout 的上下文。
幸运的是,您几乎永远不会需要上述解决方案,因为存在针对最小的和最大常量的缩写表示。这些是等效的
item >> layoutSize(100).max // width, height <= 100
item >> .max(100) // same
约束两个维度
size
目标结合了宽度和高度。它完全等同于这些单一维度
item1 >> item2.size.min // at least as big as item2
尺寸目标还可以表示常量尺寸。这些是等效的
item >> .size(100, 50) // size target with constants
item >> (100, 50) // same
还有针对最小和最大尺寸的缩写表示。这些是等效的
item >> layoutSize(100, 50).min // at least 100 by 50
item >> .min(100, 50) // same
约束到父视图
通常情况下,在良好的代码结构中,视图会添加并布局它们自己的子视图。在这些上下文中,被约束子视图的父视图(superview)是 self
,这使得将子视图与父视图的各种属性相约束变得容易。
class MySuperview: UIView {
// ... other code, including call to addSubviews() ...
func addSubviews() {
let subview = addForAutoLayout(UIView())
subview >> allButBottom // constrain 3 edges to parent (self)
subview >> height.at(0.2) // constrain height to 20% of parent (self)
}
}
有时,并非所有父视图都是实现为自己的自定义视图类。换句话说,一些自定义视图或控制器类添加并布局整个子视图层次结构。在这些上下文中,封装的自定义视图或视图控制器控制其子视图的父子关系,并可以直接将子视图约束到父视图。
class MySuperview: UIView {
// ... other code, including call to addSubviews() ...
func addSubviews() {
let subview = addForAutoLayout(UIView())
let subsubview = subview.addForAutoLayout(UIView())
subsubview >> subview.allButBottom // constrain 3 edges to parent
subsubview >> subview.height.at(0.2) // constrain height to 20% of parent
}
}
如果您仍然想要显式地将布局项约束到其父视图,可以使用 parent
属性。在视图上,parent
属性是它的 superView
。在布局指南上,parent
属性是它的 owningView
。当然,parent
是可选的,但所有基于布局项的约束目标都可以是可选的。
item >> item.parent?.top.offset(10) // constrain top to parent, inset 10
item >> item.parent?.allButBottom // constrain 3 edges to parent
item >> item.parent?.size.at(0.3) // constrain width and height to 30% of parent
item >> item.parent?.all(leadingOffset: 10) // constrain all edges to parent, leading inset 10
item >> item.parent // constrain all edges to parent
约束到 iOS 的安全区域
在 iOS 11 及以上版本中,您可以通过 safeArea
属性访问视图的安全区域,并通过可选的 parentSafeArea
属性访问父视图的安全区域。
在良好的代码结构中,在视图添加和布局它们自己的子视图时,您只需在 self
上调用 safeArea
。
class MyView: UIView {
// ... other code, including call to addSubviews() ...
func addSubviews() {
addForAutoLayout(MyContentView()) >> safeArea // constrain content to safe area
}
}
如果发现您需要约束许多子视图到安全区域,可能应该有一个包含它们的内容视图。
系统间距在iOS和tvOS
通过Apple的NSLayoutAnchor
,您可以使用神秘的“系统间距”。Apple并未公开计算方法,也不提供任何可访问的具体值。使用通过NSLayoutAnchor
API进行系统间距的操作有些不便,其应用方式有限,可应用的范围也有限。
GetLaid将系统间距以两个全局的CGFLoat
常量暴露出来。它会在您首次访问它们时调用实际的Apple API来计算这些常量。
systemSiblingSpacing
是用户的系统想要在同级视图之间形成的间距。systemParentSpacing
是用户的系统想要在视图边缘与包含的子视图之间形成的内边距。
似乎在iOS中,这两种系统间距始终相同。至少,我已经检查过从iPhone SE到最新的13英寸iPad Pro,以及从iOS 12.0到iOS 13.3的情况。因此,GetLaid还提供了一个通用的systemSpacing
,它仅返回systemSiblingSpacing
。
作为常量,系统间距提供了很大的灵活性。
item2.left >> item1.right.offset(systemSpacing) // gap between views
item >> item.parent?.top.offset(systemSpacing) // inset to parent
spacer.width >> .min(systemSpacing) // minimum spacer width
请记住,这些常量不是硬编码的,而是在实际用户的设备上动态计算的,因此它们绝对符合Apple对同级间隔和父内边距的意图,适用于任何系统和iOS/tvOS版本。但也要注意,这两个值并没有捕获与基准线和字体大小以及可能的其它上下文中联动的NSLayoutAnchor
所提供的系统间距魔法。