NamadaLayout
NamadaLayout 严格来说是 Swift 的 DSL 框架,用于使 Auto layout 更容易。但其功能远不止于此。
示例
要运行示例项目,请克隆仓库,然后首先从 Example 目录运行 pod install
。
要求
安装
NamadaLayout 可通过CocoaPods获得。要安装它,只需将以下行添加到您的 Podfile 中
pod 'NamadaLayout'
作者
Nayanda Haberty, [email protected]
许可证
NamadaLayout 在 MIT 许可证下可用。更多详细信息请参阅 LICENSE 文件。
示例项目
您可以在以下位置查看示例项目,或查看更复杂版本:https://github.com/nayanda1/Tour-Of-Heroes
使用方法
基本使用方法
以下是一些使用 NamadaLayout 进行布局的示例,我认为这足够简单,任何人都可以了解如何使用它。
class ViewController: UIViewController {
private var clickCount: Int = 0
lazy var bottomButton: UIButton = build {
$0.didClicked { [weak self] _ in
self?.clickCount += 1
self?.topLabel.text = "click count: \(self?.clickCount ?? 0)"
}
$0.setTitle("Bottom Button", for: .normal)
$0.setTitleColor(.black, for: .normal)
}
lazy var topLabel = build(UILabel.self)
.text("TOP LABEL")
.textColor(.black)
.textAlignment(.center)
.lineBreakMode(.byTruncatingMiddle)
.build()
@ViewState var topTitle: String?
@ViewState var typedText: String?
override func viewDidLoad() {
super .viewDidLoad()
layoutView()
}
func layoutView() {
layoutContent { content in
content.putSearchBar()
.placeholder("I am Placeholder")
.at(.fullTop, .equal, to: .safeArea)
.bind(\.text, with: $typedText)
content.put(topLabel)
.top(.equalTo(18), to: Anchor(of: .previous).bottomAnchor)
.centerX(.equal, to: .parent)
.horizontal(.moreThanTo(18), to: .parent)
.bind(\.text, with: $topTitle)
.whenStateChanged(for: $typedText, thenAssign: \.text, with: "typed: \(typedText ?? "null")")
content.putVStack()
.alignment(.center)
.distribution(.fill)
.spacing(9)
.center(.equal, to: .parent)
.inBetween(of: topLabel, and: bottomButton, .vertically(.moreThanTo(18)), priority: .defaultLow)
.horizontal(.moreThanTo(18), to: .parent)
.layoutContent { sContent in
sContent.putStackedLabel()
.text("MIDDLE LABEL")
.textColor(.black)
.textAlignment(.center)
.lineBreakMode(.byTruncatingMiddle)
.observe($topTitle) {
$0.text = "new: \($1.new ?? "null"), old: \($1.old ?? "null")"
}
sContent.putStackedImageView()
.image(#imageLiteral(resourceName: "anyImage"))
.contentMode(.scaleAspectFit)
.size(.equalTo(.init(width: 90, height: 90)))
}
content.put(bottomButton)
.at(.fullBottom, .equalTo(18), to: .safeArea)
.height(.equalTo(36))
}
}
}
它将自动将视图作为闭包层次结构放置,在闭包完成后创建所有约束并在 UIThread 上激活。它还将具有相同类型的视图键路径绑定到具有 @ViewState 特性的任何属性。
布局
基本
创建布局主要有两个函数:
layout(withDelegate delegate: NamadaLayoutDelegate?, _ options: SublayoutingOption, _ layouter: (ViewLayout<Self>) -> Void)
layoutContent(withDelegate delegate: NamadaLayoutDelegate? = nil, _ options: SublayoutingOption, _ layouter: (LayoutContainer
这个方法可以在 UIViewController 和 UIView 中使用。这三个方法的不同之处在于) -> Void) layout
用于布局视图的约束。layoutContent
用于布局视图内容,忽略其自身的约束。
两者都接受 SubLayoutingOption 枚举,如下
addNew
将添加新的约束,忽略视图当前的约束,这是默认值且速度最快editExisting
将编辑由 NamadaLayout 创建的现有相同约束关系,并在有新约束时添加它们。由于它实际上会检查相同约束的关系,因此其速度会比addNew
略慢removeOldAndAddNew
将删除由 NamadaLayout 创建的所有当前约束并添加新约束。由于它实际上会检查所有约束的标识符,因此其速度会比addNew
略慢cleanLayoutAndAddNew
将删除由 NamadaLayout 创建的所有当前约束并从其父视图中删除所有子视图。由于它实际上会检查所有约束的标识符并循环所有子视图以将其从父视图中删除,因此它将是最慢的方法,但也是最安全的。
我们稍后会讨论委托。
示例
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent(.editExisting) { content in
content.put(someView)
}
}
}
上述代码将 someView
添加到 ViewController 视图子视图,并编辑 ViewController 视图及其子视图中可能存在的现有约束
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent(.removeOldAndAddNew) { content in
content.put(someView)
.layoutContent { someViewContent in
someViewContent.put(someOtherview)
}
}
}
}
上述代码将 someView
添加到 ViewController 视图子视图,然后将 someOtherView
放入 someView
子视图中,并在必要时从 ViewController 视图及其子视图中删除现有约束
如果您有 stackView,您可以在内部或外部堆叠视图
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someStack)
.layoutContent { someStackContent in
someStackContent.putStacked(stackedView)
someStackContent.putSpace(by: 8)
someStackContent.putStacked(otherStacked)
someStackContent.put(someOtherview)
}
}
}
}
上述代码将 someStack
添加到 ViewController 视图子视图,然后将 stackedView
、空格为 8 点和 otherStacked
放入 stackedView
安排子视图中,并将 someOtherview
放入 someStack
中,但不堆叠。
您还可以嵌入 UIViewController
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someViewController)
}
}
}
有一些创建和添加特定视图的方法,我相信这几乎涵盖了苹果 UIKit 中的所有默认视图
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putView()
content.putView(assignedTo: &myViewVariable)
content.putLabel()
content.putLabel(assignedTo: &myLabelVariable)
}
}
}
如代码所示,将创建视图并将其自动分配给您提供的变量。这种类型的方法也适用于堆栈
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putStack().layoutContent { stackContent in
stackContent.putStackedView()
stackContent.putStackedView(assignedTo: &myViewVariable)
stackContent.putStackedTextView()
stackContent.putStackedTextView(assignedTo: &myTextViewVariable)
}
}
}
}
位置约束
所有布局都有边缘,您可以创建非常易于阅读的约束,如下所示
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.top(.equal, to: topView.bottomAnchor)
.left(.moreThan, to: leftView.rightAnchor)
.right(.lessThan, to: rightView.leftAnchor)
.bottom(.equalTo(18), to: bottomView.topAnchor)
.centerY(.moreThanTo(9), to: centerView.centerYAnchor)
.centerX(.lessThanTo(4.5), to: centerView.centerXAnchor)
}
}
}
上述代码将 someView
添加到 ViewController 视图子视图中,并创建以下约束
someView
顶部与topView
底部相等someView
左侧大于等于leftView
右侧someView
右侧小于等于rightView
左侧someView
底部等于距离bottomView
顶部 18 的距离someView
中间Y大于等于距离bottomView
中间Y 9 的距离someView
中间X小于等于距离bottomView
中间X 4.5 的距离
如果您想将视图约束与其父视图一起使用,只需传递 .parent
或 .safeArea
即可
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.top(.equal, to: .safeArea)
.left(.moreThan, to: .parent)
.right(.lessThan, to: parent)
.bottom(.equalTo(18), to: safeArea)
}
}
}
上述代码将 someView
添加到 ViewController 视图子视图中,并创建以下约束
someView
顶部与其父视图顶部相等someView
左侧大于等于其父视图左侧someView
右边的大小小于等于其父视图的右边someView
底部等于其父视图底部到其自身的距离
如果您没有指向想要约束的视图的任何变量,请使用 AnonymousRelation
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putView()
content.putView()
.top(.equal, to: Anchor(of: .previous).bottomAnchor)
.left(.equal, to: Anchor(of: .parent).leftAnchor)
}
}
}
上述代码将两个 匿名视图
添加到 ViewController 的视图子级,然后创建这些约束
第一个视图
顶部等于前一个视图
底部锚点第一个视图
左边等于父视图
左边锚点
共有 6 种 匿名关系
parent
与向父视图应用的约束相同safeArea
与向父视图的安全区域应用的约束相同myself
与向自身应用的约束相同mySafeArea
与向自身的安全区域应用的约束相同previous
与向前一个布局视图应用的约束相同previousSafeArea
与向前一个布局视图的安全区域应用的约束相同
有一些快捷方式可以一次添加多个约束,例如
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.center(.equal, to: otherView)
.vertical(.moreThanTo(18), to: .safeArea)
.horizontal(.lessThanTo(UIHorizontalInsets(left: 9, right: 18)), to: .parent)
content.put(tableView)
.edges(.equalTo(18), to: .parent)
content.put(imageView)
.inBetween(of: someView, and: otherView, .horizontally(.equal))
content.put(logoView)
.at(.topLeft, equalTo(9), to: .safeArea)
.at(.topOf(otherView), .equal)
}
}
}
center
是将centerX
和centerY
同时设置为其他视图的快捷方式vertical
是将top
和bottom
同时设置为父视图或安全区域的快捷方式horizontal
是将top
和bottom
同时设置为父视图或安全区域的快捷方式edges
是将top
、bottom
、left
和right
同时设置为父视图或安全区域的快捷方式inBetween
是将top
和bottom
或left
和right
同时设置为两个视图的快捷方式at
是将top
、bottom
、left
或right
同时设置为父视图或安全区域的快捷方式at
也可以是其他快捷方式,可以同时设置top
、bottom
、left
、right
和居中到其他视图
尺寸约束
要定义与其他对象的尺寸关系,您可以这样做:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.width(.equalTo(.parent), multiplyBy: 0.75, constant: 18)
.height(.lessThanTo(90))
.height(moreThanTo(otherView.heightAnchor))
}
}
}
上述代码将 someView
添加到 ViewController 视图子视图中,并创建以下约束
someView
宽度等于父视图,乘以 0.75,加 18。someView
高度小于等于 90someView
高度大于等于其其他视图的高度
如果您需要,可以使用 AnonymousRelation
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.width(.equalTo(.parent), multiplyBy: 0.75, constant: 18)
.height(.equalTo(Anchor(of: .parent).widhtAnchor))
}
}
}
上述代码将 someView
添加到 ViewController 视图子视图中,并创建以下约束
someView
宽度等于父视图,乘以 0.75,加 18。someView
高度等于其父视图的宽度
还有一些快捷方式
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.size(.lessThan(otherView), multiplyBy: 0.75, constant: 18)
.size(.moreThan(CGSize(widht: 90, height: 90)))
}
}
}
这可以通过上面的代码来描述
滚动自动内容约束
您可以通过使用 putScrollVContentView()
添加内容视图到 UIScrollView
并应用约束以确保内容根据内容自动调整大小
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putScroll()
.edges(.equal, to: .parent)
.layoutContent { scroll in
scroll.putScrollVContentView()
.layoutContent { scrollContent in
// do layout vertical scroll content
...
...
}
}
}
}
}
或者如果您已经有了自定义内容视图,请使用 putAsScrollVContent<View: UIView>(_ view: View)
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putScroll()
.edges(.equal, to: .parent)
.layoutContent { scroll in
scroll.putAsScrollVContent(myScrollContentView)
.layoutContent { scrollContent in
// do layout vertical scroll content
...
...
}
}
}
}
}
对于水平内容,只需使用 putScrollHContentView()
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putScroll()
.edges(.equal, to: .parent)
.layoutContent { scroll in
scroll.putScrollHContentView()
.layoutContent { scrollContent in
// do layout vertical scroll content
...
...
}
}
}
}
}
或者如果您已经有了自定义内容视图,请使用 putAsScrollHContent<View: UIView>(_ view: View)
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putScroll()
.edges(.equal, to: .parent)
.layoutContent { scroll in
scroll.putAsScrollHContent(myScrollContentView)
.layoutContent { scrollContent in
// do layout vertical scroll content
...
...
}
}
}
}
}
优先级
要定义约束优先级,只需在任何方法中传递 UILayoutPriority
即可。如果不传递优先级,它将使用简单的规则来分配优先级,即第一个约束的优先级高于第二个,以此类推。初始默认优先级为 999
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.top(.equal, to: .safeArea, priority: .required)
.left(.moreThan, to: .parent, priority: .defaultHigh)
.right(.lessThan, to: parent, priority: .defaultLow)
.bottom(.equalTo(18), to: safeArea, priority: 1000)
}
}
}
布局中构建
如果想在布局时设置视图,只需像调用设置器方法一样调用属性
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putTextView()
.backgroundColor(.white)
.text("some text")
.edges(.equal, to: .safeArea)
}
}
}
或者使用 apply
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putTextView()
.edges(.equal, to: .safeArea)
.apply {
$0.backgroundColor = .white
$0.text = "some text"
}
}
}
}
或者两者都使用
委托
当你创建布局时,可以传递一个委托
public protocol NamadaLayoutDelegate: class {
func namadaLayout(viewHaveNoSuperview view: UIView) -> UIView?
func namadaLayout(neededViewControllerFor viewController: UIViewController) -> UIViewController?
func namadaLayout(_ view: UIView, erroWhenLayout error: NamadaError)
}
由于所有默认实现都已在扩展中定义,所以所有方法都是可选的。每个方法的目的如下
namadaLayout(viewHaveNoSuperview view: UIView) ->UIView?
当你调用与父级相关联,但你的布局没有父级时(例如,在 UIViewController 的顶层视图中)会被调用。默认实现将抛出 LayoutErrorfunc namadaLayout(neededViewControllerFor viewController: UIViewController) -> UIViewController?
当你嵌入 UIViewController,但你的布局没有 UIViewController 时(例如,在 UITableViewCell 中)会被调用。默认实现将抛出 LayoutErrorfunc namadaLayout(_ view: UIView, erroWhenLayout error: NamadaError)
当在创建约束时出现 LayoutError 时会被调用。
你可以这样在第一次调用 layoutContent 或 layout 时传递委托
layoutContent(withDelegate: yourDelegate) { content in
...
...
}
分子
你可以使用 MoleculeView 协议创建分子
class MoleculeView: UIView, MoleculeView {
...
...
...
func layoutContent(_ layout: LayoutInsertable) {
layout.put(someView)
...
...
layout.put(someOtherView)
...
...
}
func moleculeWillLayout() {
//will run before layouting
...
...
}
func moleculeDidLayout() {
//will run after layouting
...
...
}
}
如果 MoleculeView
是使用 layout
或 layoutContent
添加到 superView,则将调用 layoutContent
MoleculeView 有可选的函数,包括
moleculeWillLayout()
:在布局之前运行moleculeDidLayout()
:在布局之后运行
单元格分子
您可以为单元格(UITableViewCell 或 UICollectionViewCell)创建分子。只需扩展 CollectionMoleculeCell 为 UICollectionViewCell 或 TableMoleculeCell 为 UITableViewCell。
class MyTableCell: TableCellLayoutable {
...
...
override var layoutBehaviour: CellLayoutBehaviour { .layoutOn(.reused) }
override func layoutContent(_ thisLayout: layout) {
layout.put(someView)
...
...
layout.put(someOtherView)
...
...
}
}
当单元格第一次布局时会运行 LayoutChild,或者如果您重写了它,则取决于 CellLayoutBehaviour。行为有:
layoutOnce
,这将在第一次布局时确保 layoutChild 只运行一次。layoutOn(CellLayoutingPhase)
,这将布局在第一次布局和在阶段。layoutOnEach([CellLayoutingPhase])
,这将在第一次布局和在每个阶段上布局。alwaysLayout
,这将布局在每次阶段。
阶段包括:
firstLoad
setNeedsLayout
reused
无
您可以实现 func layoutOption(on phase: CellLayoutingPhase) -> SublayoutingOption
来指定每个阶段想要的子布局选项。默认是 firstLoad 时的 .addNew
和其他情况下的 .removeOldAndAddNew
。
class MoleculeCell: TableCellLayoutable {
...
...
func layoutOption(on phase: CellLayoutingPhase) -> SublayoutingOption {
return .removeOldAndAddNew
}
}
如果您的 UITableView
或 UICollectionView
有自定义计算大小,则只需重写 UICollectionViewCell 的 calculatedCellSize(for collectionContentWidth: CGFloat) -> CGSize
或 UITableViewCell 的 calculatedCellHeight(for cellWidth: CGFloat) -> CGFloat
。
class MyCollectionCell: CollectionCellLayoutable {
...
...
//default return value is CGSize.automatic
override func calculatedCellSize(for collectionContentWidth: CGFloat) -> CGSize {
let side: CGFloat = collectionContentWidth / 3
return .init(width: side, height: side)
}
...
...
}
或针对 UITableViewCell
class MyTableCell: TableCellLayoutable {
...
...
//default return value is CGFloat.automatic
override func calculatedCellHeight(for cellWidth: CGFloat) -> CGFloat {
cellWidth / 3
}
...
...
}
绑定
为了进行绑定,您需要创建与您想绑定的视图属性相同类型的属性并添加 @ViewState
属性。请注意,唯一可以绑定的属性是 UIKit 和该库本机的属性。
@ViewState searchPhrase: String?
双向绑定
双向绑定是当属性变化时将更改应用于绑定视图,或者反之亦然。它不会作用于只能读的视图属性。
然后您可以通过 projectedValue 和 keyPath 手动绑定它:
$searchPhrase.bind(with: yourSearchBarToBind, \.text)
// or
$searchPhrase.map(from: yourSearchBarToBind, \.text)
// or
$searchPhrase.apply(into: yourSearchBarToBind, \.text)
这些方法之间的唯一区别是
bind
,它只会绑定具有属性的视图。map
,它将绑定并获取视图属性的当前值到绑定属性中。apply
,它将绑定并将绑定属性的当前值应用到视图属性中。
并且您可以添加绑定观察者
$searchPhrase.bind(with: yourSearchBarToBind, \.text)
.viewDidSet(then: { searchBar, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
// do something when view changes
}
).stateDidSet(then: { searchBar, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
// do something when state changes
}
)
当视图属性变化时,将运行 viewDidSet。当绑定属性变化时,将运行 stateDidSet。
或者您可以在布局时绑定:
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.bind(\.text, with: $typedText)
或者
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.apply(\.text, from: $typedText)
或者
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.map(\.text, into: $typedText)
单向绑定
单向绑定指的是,当绑定属性将从视图属性读取变化时,但是当绑定的属性发生变化时,不会将变化应用到视图属性。
要进行单向绑定,您需要创建一个与要绑定的视图属性相同类型的属性,并添加@ViewState
属性。
@ViewState searchPhrase: String?
然后您可以通过 projectedValue 和 keyPath 手动绑定它:
$searchPhrase.oneWayBind(with: yourSearchBarToBind, \.text)
并且您可以添加绑定观察者
$searchPhrase.oneWayBind(with: yourSearchBarToBind, \.text)
.viewDidSet(then: { searchBar, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
// do something when view changes
}
)
请注意,在进行单向绑定时,didSet状态将不适用。
或者您可以在布局时绑定:
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.oneWayBind(\.text, with: $typedText)
观察状态
如果您想观察状态的更改,可以使用@ViewState
或@ObservableState
。两者的区别在于:
- ObservableState无法绑定到UIView属性,因此如果您只想在属性上添加didSet或willSet而忽略视图中的内容,它会更好。
- ViewState可以被观察并绑定到UIView属性。但是,如果您想同时进行这两者,请注意属性的类型应与视图相同,并且它会根据视图状态自动改变,因此不要对相同的绑定属性进行赋值,因为这可能创建堆栈溢出。
@ViewState searchPhrase: String?
或者如果您想使用ObseravbleState
@ObservableState searchPhrase: String?
那么
$searhPhrase.observe(observer: self).willSet { selfObserver, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
let trigger = changes.trigger
let viewThatTriggerChanges: UIView? = trigger.triggeringView
// do something when searchPhrase is will change
}.didSet { selfObserver, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
let trigger = changes.trigger
let viewThatTriggerChanges: UIView? = trigger.triggeringView
// do something when searchPhrase is change
}
有三个触发枚举:
view(UIView)
表示闭包是从视图中的更改触发的state
表示闭包是从绑定的属性中的直接更改触发的bind
表示在绑定过程中触发闭包
您可以将didSet闭包的运行延迟如下:
$searhPhrase.observe(observer: self)
.delayMultipleSetTrigger(by: 1)
.didSet { selfObserver, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
let trigger = changes.trigger
let viewThatTriggerChanges: UIView? = trigger.triggeringView
// do something when searchPhrase is change
}
这意味着当内存中一个秒的时间间隔内触发多次设置时,它将等待一秒钟才运行下一个闭包的最新更改,并忽略那个间隔中发生的任何更改,除了最后一个,它将安排在下一个闭包运行时运行。请注意,willSet不会受到影响。
您始终可以观察get。
$searhPhrase.observe(observer: self).willGet { selfObserver, value in
// do something when searchPhrase property will get
}.didGet { selfObserver, value in
// do something when searchPhrase property did get
}
或使用相同类型的属性观察布局时的状态
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.whenStateChanged(for: $someText, thenAssignTo: \.text)
或使用自动闭合,它是跨类型属性
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.whenStateChanged(for: $someText, thenAssign: \.text, with: "assigned with \(someText)")
或使用闭包观察者
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.observe($someText) { searchBar, changes in
searchBar.text = changes.new
}
视图模型
基本视图模型
您可以通过扩展ViewModel类来创建视图模型。
class MyViewModel: ViewModel<MyView> {
@ViewState image: UIImage?
@ViewState text: String?
override func willApplying(_ view: MyView) {
// do something when view model will applying view
}
override func didApplying(_ view: MyView) {
// do something when view model did applying view
}
override func modelWillMapped(from view: MyView) {
// do something when view model will mapped view
}
override func modelDidMapped(from view: MyView) {
// do something when view model did mapped view
}
override func willUnbind() {
// do something when view model will unbind
}
override func didUnbind() {
// do something when view model did unbind
}
override func bind(with view: MyView) {
super.bind(with: view)
$image.bind(with: view.imageView, \.image)
$text.bind(with: view.textView, \.text)
$text.observe(observer: self)
.delayMultipleSetTrigger(by: 1)
.didSet { model, changes in
// do something
}
}
}
ViewModel泛型参数可以是任何扩展NSObject的对象,例如UIView或UIViewController。然后您可以使用视图模型,就像这样:
class MyView: UIViewController {
override func viewDidLoad() {
super .viewDidLoad()
layoutView()
let viewModel: MyViewModel = .init()
viewModel.apply(into: self)
}
}
您可以将视图绑定到ViewModel,就像常规绑定一样。在绑定时,它会自动设置所有@ViewState
属性,以便运行这些行为。
单元格视图模型
要创建支持单元格复用的视图模型,您可以使用CollectionViewCellModel
用于集合和TableViewCellModel
用于表格。剩下的部分与ViewModel相同,除了泛型参数。
class MyCellView: CollectionMoleculeCell {
...
...
}
class MyCellViewModel: CollectionViewCellModel<MyCellView> {
@ViewState image: UIImage?
@ViewState text: String?
override func willApplying(_ view: MyCellView) {
// do something when view model will applying view
}
override func didApplying(_ view: MyCellView) {
// do something when view model did applying view
}
override func modelWillMapped(from view: MyCellView) {
// do something when view model will mapped view
}
override func modelDidMapped(from view: MyCellView) {
// do something when view model did mapped view
}
override func willUnbind() {
// do something when view model will unbind
}
override func didUnbind() {
// do something when view model did unbind
}
override func bind(with view: MyCellView) {
super.bind(with: view)
$image.bind(with: view.image, \.image)
$text.bind(with: view.label, \.text)
}
}
要将单元格应用到UITableView
或UICollectionView
,只需将模型设置到单元格的cells属性中即可。
let cellModels: [CollectionCellModel] = items.compactMap { item in
let cellModel: MyCellViewModel = build {
$0.cellIdentifier = item.itemId
$0.image = item.image
$0.text = item.itemName
}
return cellModel
}
table.cells = cellModels
它会自动重新装载现有的单元格和新的单元格,并且仅重新装载具有不同cellIdentifier的单元格。单元格标识符可以是任何内容,只要它是可散列的。
如果您的表格或集合可分节,您可以使用单元格创建分节并将其分配给表格分节。
let firstSection: UICollectionView.Section = .init(
identifier: "first_section",
cells: items.compactMap { item in
let cellModel: MyCellViewModel = build {
$0.cellIdentifier = item.itemId
$0.image = item.image
$0.text = item.itemName
}
return cellModel
}
)
let secondSection: UICollectionView.Section = .init(
identifier: "second_section",
cells: users.compactMap { user in
let cellModel: MyOtherCellViewModel = build {
$0.cellIdentifier = user.userId
$0.image = user.image
$0.text = user.itemName
}
return cellModel
}
)
table.sections = [firstSection, secondSection]
分节标识符可以是任何内容,只要它是可散列的。
有一些默认分节可以使用,包括:
UITableView Section
和UICollectionView Section
默认普通分节UITableView.TitledSection
和UICollectionView.TitledSection
具有标题的分节UICollectionView.SupplementedSection
带有自定义头部或/和脚部的UICollectionView分节
如果您想直接获取带有UICollectionView或UITableView的默认绑定的模型,只需从模型属性中获取即可。单元格及其分节的属性实际上是UICollectionView或UITableView视图模型属性。
let tableModel: UITableView.Model = table.model
let collectionModel: UICollectionView.Model = table.model
单元格构建器
如果您使用的是TableCellBuilder
或CollectionCellBuilder
,则将分节单元格创建到UITableView
或UICollectionView
会更简单。
table.sections = TableCellBuilder(sectionId: "first_section").
next(type: MyCellModel.self, from: myItems) { cellVM, item in
// do apply item into cell view model
}.next(type: AnyCell.self, from: myItems) { cell, item in
// do apply item into cell
// this closure is escaping and will run when cell is first created or for every reused
}.nextEmptyCell(with: cellHeight) { cell in
cell.contentView.backgroundColor = .gray
// do any setup to empty cell here
// this closure is escaping and will run when cell is first created or for every reused
}.nextSection(sectionId: "second_section")
.next(type: MyCellModel.self, from: myItems) { cellVM, item in
// do apply item into cell view model
}.build()
您也可以使用这个功能来添加现有的单元格。
collection.sections = collection.sections.append()
next(type: MyCellModel.self, from: myItems) { cellVM, item in
// do apply item into cell view model
}.next(type: AnyCell.self, from: myItems) { cell, item in
// do apply item into cell
// this closure is escaping and will run when cell is first created or for every reused
}.nextEmptyCell(with: cellSize) { cell in
cell.contentView.backgroundColor = .gray
// do any setup to empty cell here
// this closure is escaping and will run when cell is first created or for every reused
}.nextSection(sectionId: "second_section")
.next(type: MyCellModel.self, from: myItems) { cellVM, item in
// do apply item into cell view model
}.build()
ObservableView
如果您想通过代理使用 ViewModel
来观察任何视图,您可以简单地实现 ObservableView
并提供 Observer
。它将有一个名为 observer
的变量,该变量是当前绑定的 ViewModel,并将其转换为 Observer
类型。因此,别忘了在您的 ViewModel 中实现 ObserverType。最好让 Observer 扩展 ViewModelObserver
,因为它有方法通知 ViewModel 完成了布局,然后如果类型匹配,ViewModel 将自动应用观与 ViewModel。
protocol MyScreenObserver: ViewModelObserver {
func myScreen(_ screen: MyScreen, didPullToRefresh refreshControl: UIRefreshControl)
}
class MyScreen: UIViewController, ObservableView {
typealias Observer = MyScreenObserver
...
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutView()
// will apply MyScreen with any binded ViewModel if already binded
// ViewModel class are implement ViewModelObserver
observer?.viewDidLayouted(self)
}
@objc func didPullToRefresh() {
observer?.myScreen(self, didPullToRefresh: refreshControl)
}
}
其他特性
NamadaLayout 有点其他特性:
视图构建器
所有视图都实现了 Buildable
协议,这意味着视图可以通过构建函数进行实例化,例如这样
lazy var button: UIButton = build {
$0.setTitle("My Button", for: .normal)
$0.setTitleColor(.black, for: .normal)
}
或者这样,它使用 dynamicMemberLookup
确保您可以像函数分配一样调用任何属性名
lazy var topLabel = build(UILabel.self)
.text("any text")
.textColor(.black)
.textAlignment(.center)
.build()
或者如果您的对象不是 Buildable
,只需在构建函数中实例化您的视图
lazy var topLabel = build(MyObject())
.text("any text")
.build()
TextCompatible
可以使用 TextCompatible 来分配 UILabel、UITextField 和 UITextView,它是在 NSAttributedString 或 String 上实现的。
label.textCompat = "some text"
texView.textCompat = someAttributedString
textField.placeholderCompat = "some"
ImageCompatible
可以使用 ImageCompatible 来分配 UIImageView,它是在 UIImage 和 UIImageView.Animation 上实现的。
imageView.imageCompat = someImage
imageAnimated.imageCompat = UIImageView.Animation(images: seriesOfImages, duration: 1, animating: true, repeatCount: Int = 2)
UIButton 点击观察者
UIButton 有 didClicked 函数,可以分配闭合来观察 UIButton 的点击事件。
button.didClicked(then: { _ in
// do something
})
UICollectionView 和 UITableView 重新加载观察者
UICollectionView 和 UITableView 模型有 didReload 函数,可以分配闭合来监听重新加载完成时的事件。
collectionView.model.didReload(then: { _ in
// do something
})
贡献
你知道怎么做的,克隆并提交 pull request。