NamadaLayout 2.1.11

NamadaLayout 2.1.11

nayanda1维护。



  • nayanda

NamadaLayout

NamadaLayout 严格来说是 Swift 的 DSL 框架,用于使 Auto layout 更容易。但其功能远不止于此。

CI Status Version License Platform

示例

要运行示例项目,请克隆仓库,然后首先从 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) -> Void) 这个方法可以在 UIViewController 和 UIView 中使用。这三个方法的不同之处在于
  • 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 是将 centerXcenterY 同时设置为其他视图的快捷方式
  • vertical 是将 topbottom 同时设置为父视图或安全区域的快捷方式
  • horizontal 是将 topbottom 同时设置为父视图或安全区域的快捷方式
  • edges 是将 topbottomleftright 同时设置为父视图或安全区域的快捷方式
  • inBetween 是将 topbottomleftright 同时设置为两个视图的快捷方式
  • at 是将 topbottomleftright 同时设置为父视图或安全区域的快捷方式
  • at 也可以是其他快捷方式,可以同时设置 topbottomleftright 和居中到其他视图

尺寸约束

要定义与其他对象的尺寸关系,您可以这样做:

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 高度小于等于 90
  • someView 高度大于等于其其他视图的高度

如果您需要,可以使用 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 的顶层视图中)会被调用。默认实现将抛出 LayoutError
  • func namadaLayout(neededViewControllerFor viewController: UIViewController) -> UIViewController? 当你嵌入 UIViewController,但你的布局没有 UIViewController 时(例如,在 UITableViewCell 中)会被调用。默认实现将抛出 LayoutError
  • func 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 是使用 layoutlayoutContent 添加到 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
    }
}

如果您的 UITableViewUICollectionView 有自定义计算大小,则只需重写 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)
    }
}

要将单元格应用到UITableViewUICollectionView,只需将模型设置到单元格的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 SectionUICollectionView Section默认普通分节
  • UITableView.TitledSectionUICollectionView.TitledSection具有标题的分节
  • UICollectionView.SupplementedSection带有自定义头部或/和脚部的UICollectionView分节

如果您想直接获取带有UICollectionView或UITableView的默认绑定的模型,只需从模型属性中获取即可。单元格及其分节的属性实际上是UICollectionView或UITableView视图模型属性。

let tableModel: UITableView.Model = table.model
let collectionModel: UICollectionView.Model = table.model

单元格构建器

如果您使用的是TableCellBuilderCollectionCellBuilder,则将分节单元格创建到UITableViewUICollectionView会更简单。

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。