Draftsman 是一个用于 Swift 的 DSL 框架,专注于构建器模式。如果您仍然在使用 2.3.x 版本,请参考独立的 README 文件:这里。如果您仍然使用 Swift 5.1,请使用 1.1.x 版本。独立的 README 文件:这里。
要运行示例项目,请克隆 repo,然后首先从 Example 目录运行 pod install
。
Draftsman 可以通过 CocoaPods 获得。 要安装它,只需将以下行添加到您的 Podfile 中
pod 'Draftsman', '~> 3.2.5'
在 Package.swift 中将 Draftsman 添加为您的 target 依赖项
dependencies: [
.package(url: "https://github.com/hainayanda/Draftsman.git", .upToNextMajor(from: "3.2.5"))
]
在您的 target 中将它用作 Draftsman
.target(
name: "MyModule",
dependencies: ["Draftsman"]
)
Nayanda Haberty, hainayanda@outlook.com
Draftsman 在 MIT 许可证下可用。 有关更多信息,请参见 LICENSE 文件。
Draftsman 是 NSLayoutConstraints
和 UIView
层级结构的构建器。 Draftsman 使用 Swift 中的新的 resultBuilder,使声明式方法成为可能。
创建约束非常容易。 您所需要做的就是调用 drf
以获取 LayoutDraft
对象
myView.drf
.left.equal(to: otherView.drf.right)
.right.equal(with: .parent).offset(by: 16)
.top.lessThan(with: .safeArea).offSet(8)
.bottom.moreThan(with: .top(of: .keyboard))
.apply()
有两种方法可以结束规划约束,这两种方法都可以从任何 UIView
或 UIViewController
中调用
func apply() -> [NSLayoutConstraint]
func build() -> [NSLayoutConstraint]
两者之间的区别在于 apply
将激活约束,而 build
只会创建约束而不激活它们。 Apply 返回值是可丢弃的,因此您可以选择使用创建的 NSLayoutConstraint
或不使用。
您可以始终创建一个 UIViewController
或 UIView
并实现 Planned
协议,并在您希望应用 viewPlan
时调用 applyPlan()
import Draftsman
class MyViewController: UIViewController, Planned {
var models: [MyModel] = []
@LayoutPlan
var viewPlan: ViewPlan {
VStacked(spacing: 32) {
if models.isEmpty {
MyView()
MyOtherView()
SomeOtherView()
} else {
for model in models {
MyModeledView(model)
}
}
}
.centered()
.matchSafeAreaH().offset(by: 16)
.vertical.moreThan(with: .safeArea).offset(by: 16)
}
override func viewDidLoad() {
super.viewDidLoad()
applyPlan()
}
}
ViewPlan
始终可以组合以使代码更简洁
import Draftsman
class MyViewController: UIViewController, Planned {
var models: [MyModel] = []
@LayoutPlan
var viewPlan: ViewPlan {
VStacked(spacing: 32) {
stackPlan
}
.centered()
.matchSafeAreaH().offset(by: 16)
.vertical.moreThan(with: .safeArea).offset(by: 16)
}
@LayoutPlan
var stackPlan: ViewPlan {
if models.isEmpty {
emptyStackPlan
} else {
modeledStackPlan(for: models)
}
}
@LayoutPlan
var emptyStackPlan: ViewPlan {
MyView()
MyOtherView()
SomeOtherView()
}
@LayoutPlan
func modeledStackPlan(for models: [MyModel]) -> ViewPlan {
for model in models {
MyModeledView(model)
}
}
override func viewDidLoad() {
super.viewDidLoad()
applyPlan()
}
}
使用 UITableView 或 UICollectionView? 使用 Draftsman 和 Combine 轻松完成
import Draftsman
import Combine
class MyViewController: UIViewController, Planned {
@Published var models: [MyModel] = []
@LayoutPlan
var viewPlan: ViewPlan {
Tabled(forCell: MyTableCell.self, observing: $models) { cell, model in
cell.apply(model)
}
.fillParent()
}
override func viewDidLoad() {
super.viewDidLoad()
applyPlan()
}
}
您可以通过使用 draftContent
或 drf.insert
方法以及子视图 draft 的 insert
方法(如果是 UIStackView
中的 arranged subviews,则使用 draftStackedContent
或 drf.insertStacked
和 insertStacked
)来创建视图层级结构,同时创建约束。 不要忘记调用 apply()
或 build()
,两者都将重新排列视图层级结构,但只有 apply()
将激活创建的约束。
view.draftContent {
UIView().drf
.center.equal(to: .parent)
.horizontal.equal(to: .safeArea)
.vertical.moreThan(with: .safeArea)
.insert {
myView.drf
.edges(.equal, to: .parent)
}
}.apply()
视图的层级结构就像闭包在代码中声明的方式一样。 上面的代码实际上将按顺序执行以下指令
view
创建并插入一个新的 UIView()
UIView
然后将创建约束UIView
然后将插入 myView
myView
然后将创建约束因此,如果层级结构以伪层级结构样式编写,则应类似于此
view
|____new UIView
| |____myView
可以在闭包中传递的兼容类型是
UIView
的任何子类UIViewController
的任何子类如果您传递 UIViewController
,它将自动将 UIViewController
视图添加为子视图,并将 UIViewController
作为其当前 UIViewController
的子视图。 您可以根据需要插入尽可能多的组件,它将适合所有 View,就像您编写它们的方式一样。
在大多数情况下,您可以通过调用属性名称,然后再次调用 drf
来返回 Draftsman,从而使用 Draftsman 中内置的 Builder 库来构建视图
myView.drf
.backgroundColor(.black)
.drf.bottom.moreThan(to: .safeArea)
.center.equal(to: .parent)
如果属性未映射,则可以在调用属性名称之前调用 builder
myView.drf.builder
.backgroundColor(.black)
.drf.bottom.moreThan(to: .safeArea)
.center.equal(to: .parent)
在大多数情况下,您可以通过调用属性名称,然后调用 storeAll(in:)
或 stored()
来返回 Draftsman,从而使用 Combine Publisher 自动赋值给属性
@Published var myText: String?
@Published var myColor: UIColor?
var cancellables: Set<AnyCancellable> = .init()
...
...
UILabel().drf
.text(assignedBy: $myText)
.textColor(by: $myColor)
.storeAll(in: &cancellables)
.center.equal(to: .parent)
.bottom.moreThan(to: .safeArea)
storeAll(in:)
和 stored()
之间的区别在于,stored()
将可取消的对象保留在视图本身中。 如果您只想使用此方法订阅此属性一次,最好使用此方法,除非您知道自己在做什么。
如果属性未映射,则可以在调用属性名称之前调用 subscriber
UILabel().drf.subscriber
.text(assignedBy: $myText)
.textColor(by: $myColor)
.storeAll(in: &cancellables)
.center.equal(to: .parent)
.bottom.moreThan(to: .safeArea)
定位 View 很容易。 您只需要声明哪个锚点应该与其他锚点具有关系
myView.drf
.top.equal(to: other.drf.top)
.right.moreThan(to: other.drf.right).offset(by: 8)
.bottom.lessThan(to: other.drf.bottom).offset(by: 8).priority(.required)
.left.equal(to: other.leftAnchor)
.centerX.moreThan(to: other.centerXAnchor).inset(by: 8)
.centerY.lessThan(to: other.centerYAnchor).inset(by: 8).identifier("centerY")
Draftsman 中可用的基本位置锚点是
所有这些都适用于 UIView
和 UILayoutGuide
。 这可用于使用以下三种方法之一创建约束
这些方法可以接受来自 UIKit
的基本 NSLayoutAnchor
,也可以使用来自 Draftsman
的 Anchor
,只要它在同一轴上。 要添加常量,请使用 offset(by:)
或 inset(by:)
方法之一。 offset是到锚点外部的间距,
inset` 是到锚点内部的间距
对于中心锚点,offset 和 inset 可以用这张图来描述
然后,您可以为创建的约束添加优先级和/或标识符。
确定 View 的尺寸很容易。 您只需要声明哪个锚点应该与其他锚点或常量具有关系
myView.drf
.height.equal(to: other.drf.width)
.width.moreThan(to: other.drf.height).added(by: 8)
.height.lessThan(to: anyOther.heightAnchor).substracted(by: 8).priority(.required)
.width.equal(to: anyOther.widthAnchor).multiplied(by: 0.75).identifier("width")
Draftsman 中可用的基本尺寸锚点是
所有这些都适用于 UIView
和 UILayoutGuide
。 这可用于使用以下三种方法之一创建约束
这些方法可以接受来自 UIKit
的基本 NSLayoutDimension
,也可以使用来自 Draftsman
的尺寸 Anchor
。 要添加常量,请使用 added(by:)
、substracted(by:)
或 multiplied(by: )
方法之一。 然后,您可以为创建的约束添加优先级和/或标识符。
也可以使用常量来实现尺寸确定
myView.drf
.height.equal(to: 32)
.width.moreThan(to: 64)
.width.lessThan(to: 128).priority(.required).identifier("width")
非常相似,只是它接受 CGFloat
使用多个锚点创建约束非常容易,您可以随时组合两个或多个锚点,并使用它们一次创建多个约束
myView.drf
.top.left.equal(to: other.drf.top.left)
.bottom.left.right.moreThan(to: anyOther.drf.top.left.right)
它类似于单个锚点,但您只能传递具有相同轴组合的 Draftsman Anchor
有一些锚点组合的快捷方式
示例
myView.drf
.vertical.equal(to: other.drf.vertical)
.bottom.horizontal.moreThan(to: anyOther.drf.top.horizontal)
如果需要,也可以使用 CGSize
来实现使用 size 或 width.height 进行尺寸确定
myView.drf
.size.equal(to: CGSize(sides: 30))
对于 offsets 和 insets,CGFloat
与所有参数兼容。 但是,如果您需要显式地为每个边缘分配它,您可以始终传递其他内容
CGPoint
的类型别名CGPoint
的类型别名UIEdgeInsets
的类型别名UIEdgeInsets
的类型别名您可以只传递 UIView
或 UILayoutGuide
而不是显式地传递 Anchor
,它将使用相同的锚点来创建约束
myView.drf
.vertical.equal(to: otherView)
.bottom.horizontal.moreThan(to: view.safeAreaLayoutGuide)
在上面的示例中,它将在 myView
垂直锚点和 otherView
垂直锚点之间创建相等约束,然后它将使用 myView
底部和 view.safeAreaLayoutGuide
底部创建另一个。
有时您不想甚至不能显式地使用锚点。 在这种情况下,您可以随时使用 AnonymousLayout
myView.drf
.top.left.equal(with: .parent)
.bottom.moreThan(with: .safeArea).offset(by: 16)
.size.lessThan(with: .previous)
可用的 AnonymousLayout
是
它与常规锚点相同,但它将自动获取匿名视图的相同锚点。 如果要显式地获取匿名视图的不同锚点,则可以执行以下操作
myView.drf
.top.equal(with: .top(of:.parent))
.bottom.moreThan(with: .bottom(of: .safeArea)).offset(by: 16)
.width.lessThan(with: .height(of: .previous))
可用的显式锚点是
有一些快捷方式用于构建布局约束,可以通过 drf
访问
edges.equal(with: .parent)
的快捷方式edges.equal(with: .safeArea)
的快捷方式horizontal.equal(with: .parent)
的快捷方式vertical.equal(with: .parent)
的快捷方式horizontal.equal(with: .safeArea)
的快捷方式vertical.equal(with: .safeArea)
的快捷方式size.equal(with: .parent)
的快捷方式center.equal(with: .parent)
的快捷方式centerX.equal(with: .parent)
的快捷方式centerY.equal(with: .parent)
的快捷方式top.left.equal(with: .parent)
,或任何其他角落的快捷方式width.equal(with: .height(of: .mySelf))
的快捷方式height.equal(with: .width(of: .mySelf))
的快捷方式size.equal(with: givenSize)
的快捷方式使用 Draftsman 可以很容易地处理 UITableView
和 UICollectionView
。只需调用 renderCells
和任何 Hashable 序列
UITableView().drf.renderCells(using: myArrayOfHashables) { item in
MyTableCell.render { cell in
cell.apply(with: item)
}
}
为不同的项使用多个单元格?只需渲染它们
UITableView().drf.renderCells(using: myArrayOfEnum) { item in
switch item {
case .typeOne:
MyTableCellOne.render { cell in
cell.apply(with: item)
}
case .typeTwo:
MyTableCellTwo.render { cell in
cell.apply(with: item)
}
}
}
这也适用于 UICollectionView
UICollectionView().drf.renderCells(using: myArrayOfHashables) { item in
MyCollectionCell.render { cell in
cell.apply(with: item)
}
}
此功能由 UITableViewDiffableDataSource
和 UICollectionViewDiffableDataSource
提供支持。
如果使用分段的 UITableView
或 UICollectionView
,请改用 renderSections
并传递 SectionCompatible
序列
UITableView().drf.renderSections(using: myArrayOfSectionCompatibles) { item in
MyTableCell.render { cell in
cell.apply(with: item)
}
}
SectionCompatible
是一个像这样声明的协议
public protocol SectionCompatible {
associatedtype Identifier: Hashable
associatedtype Item: Hashable
var identifier: Identifier { get }
var items: [Item] { get }
}
如果不希望实现 SectionCompatible
,可以使用 Sectioned
结构体代替
Sectioned(myIdentifier, items: myArrayOfItems)
通常情况下,单元格会随着时间的推移而变化,您不想再次渲染整个表。在这种情况下,您可以使用 Combine Publisher
而不是 Sequence
@Published var myItems: [Item] = []
...
...
UITableView().drf.renderCells(observing: $myItems) { item in
MyTableCell.render { cell in
cell.apply(with: item)
}
}
每当发布者发布更改时,表视图将根据发布的项目进行更新。 这也适用于 renderSections
@Published var mySections: [Sectioned<MySection, Item>] = []
...
...
UITableView().drf.renderSections(observing: $mySections) { item in
MyTableCell.render { cell in
cell.apply(with: item)
}
}
所有这些都适用于 UITableView
和 UICollectionView
您可以将 SpacerView 用作 UIStackView 内容的间隔符
UIScrollView().drf.insertStacked {
MyView()
SpacerView(12)
OtherView()
}
或者,如果您希望间隔符大小是动态的,则可以留空 init
UIScrollView().drf.insertStacked {
MyView()
SpacerView()
OtherView()
}
有一个名为 ScrollableStackView
的自定义 UIView
,它是 UIScrollView
内的 UIStackView
。 如果您需要一个 UIStackView
,当内容大于容器时可以滚动,您可以使用它。 它有 2 个可以使用的公共 init
除此之外,它可以像常规 UIStackView
和常规 UIScrollView
一样使用,只是无法更改其 distribution,因为它需要确保视图按预期运行。
如果您希望 viewPlan
更短且不太明确,可以使用一些助手。 此助手将接受 LayoutPlan
闭包,因此您无需通过 drf
访问它,而是直接在其 init 中访问
HStacked
和 VStacked
是创建垂直和水平 UIStackView 的快捷方式,而无需显式创建。 它有 3 个可以使用的公共 init
示例
VStacked(distribution: .fillEqually) {
SomeView()
MyView()
OtherView()
}
.fillParent()
这将等同于
UIStackView(axis: .vertical, distribution: .fillEqually).drf
.fillParent()
.insertStacked {
SomeView()
MyView()
OtherView()
}
HScrollableStacked
和 VScrollableStacked
是创建垂直和水平 ScrollableStackView
的快捷方式,而无需显式创建。 它有 3 个可以使用的公共 init
示例
HScrollableStacked(alignment: .fill) {
SomeView()
MyView()
OtherView()
}
.fillParent()
这将等同于
ScrollableStackView(axis: .horizontal, alignment: .fill).drf
.fillParent()
.insertStacked {
SomeView()
MyView()
OtherView()
}
可以使用 Tabled
或 Collectioned
在 init 时创建和调用 renderCells
、renderSections
(均在 UITableView
和 UICollectionView
上)
Tabled(forCell: MyCell.self, observing: $items) { cell, item in
cell.apply(item)
}
这将等同于
UITableView().drf.renderCells(observing: $items) { item in
MyCell.render { cell in
cell.apply(with: item)
}
}
与 UICollectionView
相同
Collectioned(forCell: MyCell.self, observing: $items) { cell, item in
cell.apply(item)
}
Margined
是一种向任何 UIView
添加边距的简单方法。 例子
Margined(by: 12) {
MyView()
}
.fillParent()
这将等同于
UIView().drf.builder
.backgroundColor(.clear)
.drf.fillParent()
.insert {
MyView().fillParent().offsetted(by: 12)
}
Draftsman Planned
协议是一个协议,它使任何 UIView
或 UIViewController
都可以拥有其预定义的视图计划,并使用 applyPlan
方法应用它。 该协议声明如下
public protocol Planned: AnyObject {
var planIdentifier: ObjectIdentifier { get }
var appliedConstraints: [NSLayoutConstraint] { get }
var viewPlanApplied: Bool { get }
@LayoutPlan
var viewPlan: ViewPlan { get }
@discardableResult
func applyPlan() -> [NSLayoutConstraint]
}
您只需要实现 viewPlan
getter,因为所有内容都将在扩展中实现
import Draftsman
class MyViewController: UIViewController, Planned {
@LayoutPlan
var viewPlan: ViewPlan {
VStacked(spacing: 32) {
MyView()
MyOtherView()
SomeOtherView()
}
.centered()
.matchSafeAreaH().offset(by: 16)
.vertical.moreThan(with: .safeArea).offset(by: 16)
}
override func viewDidLoad() {
super.viewDidLoad()
applyPlan()
}
}
每次调用 applyPlan
时,它都会始终尝试重新创建视图,使其与 viewPlan
中声明的内容相同。
有一些带有 Planned
的类型别名可以使用
UIViewController & Planned
UIView & Planned
PlannedCell
是专门为单元格构建的 Planned
,声明如下
public protocol PlannedCell: Planned {
@LayoutPlan
var contentViewPlan: ViewPlan { get }
}
您只需要实现 contentViewPlan
getter,因为所有内容都将在扩展中实现。 它将跳过 contentView
并直接进入其内容
class TableCell: UITableView, PlannedCell {
@LayoutPlan
var contentViewPlan: ViewPlan {
HStacked(margin: 12, spacing: 8) {
UIImageView(image: UIImage(named: "icon_test")).drf
.sized(CGSize(sides: 56))
VStacked(margin: 12, spacing: 4) {
UILabel(text: "title text")
UILabel(text: "subtitle text")
}
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
applyPlan()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
applyPlan()
}
}
每次调用 applyPlan
时,它都会始终尝试重新创建视图,使其与 viewPlan
中声明的内容相同。
有一些带有 Planned
的类型别名可以使用
UITableViewCell & PlannedCell
UICollectionViewCell & PlannedCell
PlannedStack
是专门为单元格构建的 Planned
,声明如下
public protocol PlannedStack: Planned {
@LayoutPlan
var stackViewPlan: ViewPlan { get }
}
您只需要实现 stackViewPlan
getter,因为所有内容都将在扩展中实现。 它将自动将计划视为堆栈的 arrangeSubviews
class MyStack: UIStackView, PlannedStack {
@LayoutPlan
var stackViewPlan: ViewPlan {
UIImageView(image: UIImage(named: "icon_test"))
.sized(CGSize(sides: 56))
UILabel(text: "title text")
UILabel(text: "subtitle text")
}
override init(frame: CGRect) {
super.init(frame: frame)
applyPlan()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
applyPlan()
}
}
每次调用 applyPlan
时,它都会始终尝试重新创建视图,使其与 viewPlan
中声明的内容相同。
您可以使用 UIPlannedStack
,因为它是 UIStackView & PlannedStack
的类型别名
您知道如何操作,只需克隆并进行拉取请求即可