DeclarativeLayoutKit 3.0.3

DeclarativeLayoutKit 3.0.3

Ernest0N 维护。



  • Ernest

DeclarativeLayoutKit

一个简单且轻量级的工具框架,用于快速和声明式 UI 布局。

注意:它由 SnapKit 支持!

优势
🚀 使用 属性链 快速进行视图配置
☺️ 使用最简单的 DSL 进行布局,并与 SnapKit DSL 混合
🎁 额外功能 - 堆叠视图布局,类似于 UIStackView,但更灵活
🌈 可重用且可混合的 视图样式
🧩 完全 可扩展

概述

要求

  • iOS 10.0+
  • Xcode 10.0+
  • Swift 5.0+

使用方法

🚀属性链

在函数中,绝对全部的可变属性都是通过由 Sourcery 驱动的代码生成来表示。

let myLabel = UILabel()
    .numberOfLines(0)
    .text("Hello buddy")
    .backgroundColor(.blue)
    .isHighlighted(true)
    .borderWidth(1)
    .borderColor(.cyan)

目前,属性链式调用支持以下类型:UIViewUIControlUILabelUIImageViewUIScrollViewUITextViewUITableViewUICollectionViewUITextFieldUIButtonUISliderUISwitchUIStackView

您还可以轻松地为其他类型生成函数 - 查看方法

😋并且还有一些额外的语法糖

可分配给变量

class ViewController: UIViewController {
    weak var myLabel: UILabel!

    override func loadView() {
        ...
        view.addSubview(
            UILabel()
                .numberOfLines(0)
                .text("Voila")
                // sets a reference of object that calls function(in this case, created UILabel instance) to passed variable
                .assign(to: &myLabel)
                ...
        )
    }
}

基于闭包的动作和手势

UIControl()
    .addAction(for: .valueChanged, { print("value changed") })

UIButton()
    .title("Tap me")
    .onTap({ print("didTap") })

UIView()
    .onTapGesture({ print("Kek") })
    .onLongTapGesture({ print("Cheburek") })

// ⚠️ Don't forget about ARC when use some parent view in action closure, to prevent retain cycle

简单的UIStackView辅助工具

  • HStackView - UIStackView().axis(.horizontal)
  • VStackView - UIStackView().axis(.vertical)

声明性锚点构建器

您可以使用相同的锚点样式设置约束。返回类型将是AnchorLayoutBuilder - 一个简单的容器,用于存储声明的锚点常量。要应用它们,只需调用build()函数。

let myLabel = UILabel()
    .numberOfLines(0) // -> UIView
    ...
    .heightAnchor(0) // -> AnchorLayoutBuilder (same below)
    .topAnchor(16.from(anotherView.snp.bottom))
    .leftAnchor(24)
    .rightAnchor(24.orLess.pririty(750))
    .build() // -> UIView (with applyed constraints)

框架允许两种设置常量的方式

let myLabel = UILabel()
    .numberOfLines(0)
    .layout({
        $0.height.equalTo(0)
        $0.top.equalTo(anotherView.snp.bottom).inset(16)
        $0.left.equalToSuperview().inset(14)
        $0.right.lessThanOrEqualToSuperview().inset(24)
    })
    .build()
  • 自带的由AnchorLayoutBuilderConstraint驱动的DSL

它具有以下模板
inset.from(SnapKit.ConstraintPriorityTarget).priority(UILayoutPriority)

如果只指定inset,它将应用于superview

myView.horizontalAnchor(16).topAnchor(0).bottomAnchor(44)

如果您想更改比较类型(小于、大于或等于),在inset后添加.orLess.orGreater后缀

myView.bottomAnchor(44.orLess).rightAnchor(8.orGreater.from(secondView))

⚠️ AnchorLayoutBuilderConstraint的默认优先级为999! 这样决定是为了确保当无法应用时,在将来更新布局时,约束会自动重新激活

锚点函数的完整列表

width/height/left/right/top/bottom/centerX/centerYAnchor - 完全等同于NSLayoutConstraint锚点

sizeAnchor(CGSize) == widthAcnhor + heightAnchor

aspectRatioAnchor(multiplier:) == 高 / 宽

horizontalAnchor == leftAnchor + rightAnchor

verticalAnchor == topAnchor + bottomAnchor

centerAnchor == centerXAnchor + centerYAnchor

edgeAnchors(insets: UIEdgeInsets, to target: SnapKit.ConstraintRelatableTarget?) - 将所有边缘拉伸到target。默认的targetnil,与superview等价。

视图/构建器组合

您还可以将views构建器添加到superview构建器中,如下所示

weak var avatarView: UIImageView!
...
let profileView = UIView()
    .backgroundColor(.gray)
    .heightAnchor(100)
    .add({
        UIImageView()
            .assign(to: &avatarView)
            .contentMode(.scaleAspectFit)
            .sizeAnchor(CGSize(width: 40, height: 40))
            .leftAnchor(16)
            .verticalAnchor(0)

        UILabel()
            .numberOfLines(2)
            .rightAnchor(0)
            .leftAnchor(8.from(avatarView).priority(.required))
    })
    // current view is build first, and then it's subviews
    .build()

或者使用方便的初始化器

let profileView = UIView({
    UIImageView()
        .assign(to: &avatarView)
        .contentMode(.scaleAspectFit)
        .sizeAnchor(CGSize(width: 40, height: 40))
        .leftAnchor(16)
        .verticalAnchor(0)

    UILabel()
        .numberOfLines(2)
        .rightAnchor(0)
        .leftAnchor(8.from(avatarView).priority(.required))
})
.backgroundColor(.gray)
.heightAnchor(100)
// current view is build first, and then it's subviews
.build()

注意:如果在UIView中添加构建器(即在不调用add(...)之前不使用anchor-chaining),则构建将在添加后立即在superview中发生。换句话说,build()函数的返回类型将是Self(即UIView

let profileView = UIView()
    .backgroundColor(.gray)
    .add({
        UIImageView()
            .assign(to: &avatarView)
            .contentMode(.scaleAspectFit)
            .sizeAnchor(CGSize(width: 40, height: 40))
            .leftAnchor(16)
            .verticalAnchor(0)
        UILabel()
            .numberOfLines(2)
            .rightAnchor(0)
            .leftAnchor(8.from(avatarView).priority(.required))
    }) // -> UIView (with already added subviews)
    // and you can continue chainable-configuration (for example by specifying own anchors)
    .heightAnchor(100) // -> AnchorLayoutBuilder
    .build() // -> UIView

🎁额外奖励!声明性堆叠构建器

有时候,UIStackView的特性不足以实现对具有单独分布视图进行更为灵活的堆叠。

StackingLayoutBuilder将帮助您解决这个问题!

let profileView = HStack {
    UIImageView()
        .assign(to: &avatarView)
        .contentMode(.scaleAspectFit)
        .sizeAnchor(CGSize(width: 40, height: 40))
        .leftSpace(12) // 👀
        .verticalAlignment(.center) // 👀

    UILabel()
        .numberOfLines(2)
        .leftSpace(8) // 👀
        .rightSpace(0) // 👀
}
.backgroundColor(.gray)

HStackVStack是一个将视图(或AnchorLayoutBuilder)收集起来,并通过AutoLayout常量相互堆叠,然后返回结果的UIView的功能。

您可以指定:

left/rightSpace(在HStack中),top/bottomSpace(在VStack中)——安排视图的前后空间。

verticalAlignment(在HStack中),horizontalAlignment(在VStack中)——在横向轴上安排视图的分布。

Alignment类型可以是:

  • .center - 横向轴上居中。

  • .fill(sideInset: AnchorLayoutBuilderConstraint = 0) - 在横向轴上拉伸。

  • HStack(类似地,在VStack中)

    • .custom(top: bottom:) - 自定义顶部和底部内边距(即AnchorLayoutBuilderConstraint)。
    • .bottom(...) - 仅设置底部内边距。
    • .top(..) - 仅设置顶部内边距。

您可以通过继续使用锚点链式来添加额外的约束,但必须在指定.space().alignment()属性之前!

🌈声明式视图样式

您不再需要创建许多样式不同的视图子类,并深入到复杂的继承层次结构。

DeclarativeLayoutKit提供了一种简单的基于闭包的解决方案,用于定义样式,并且具有结合这些样式的能力。

UIButton()
    .title("Tap me")
    .set(style: .touchHiglighting(color: .blue), .primaryRounded())
    .onTap({ ... })

就像静态工厂一样,只需在ViewStyle扩展中创建新的ViewStyle<Target>

extension ViewStyle {
    /// Generic definition is required to be able to support view subclass types
    static func primaryRounded<T: UIView>() -> ViewStyle<T> {
        ViewStyle<T>({ view in
            view.backgroundColor(.systemBlue)
                .cornerRadius(8)
                .borderWidth(4)
                .shadowColor(.darkGray)
                .shadowOpacity(1)
                .shadowRadius(5)
        })
    }

    static func touchHiglighting<T: UIButton>(color: UIColor) -> ViewStyle<T> {
        ViewStyle<T> { button in ... }
    }
}
关于ViewStyle

ViewStyle只是闭包的包装。它没有任何特别之处。🤗

public final class ViewStyle<Target: ViewStyleCompatible> {
    private let handler: (Target) -> ()

    public init(_ handler: @escaping (Target) -> ()) {
        self.handler = handler
    }

    func apply(into target: Target) {
        handler(target)
    }
}

🔥全部内容

最后,一个使用该框架进行餐厅预览布局的完整示例

布局

🧩如何扩展功能?

添加到其他类型的属性链式
  • 第一种方式 - 编写返回self的函数的类型扩展
extension MyCustomView {
    func myProperty(_ value: ValueType) -> Self {
        self.myProperty = value
        return self
    }
}
扩展AnchorLayoutBuilder

只需将扩展添加到AnchorLayoutBuilderConvertible类型中,并使用SnapKit DSL编写您自己的帮助函数。

extension AnchorLayoutBuilderConvertible {
    func leftTopAnchor(_ inset: CGFloat) -> AnchorLayoutBuilder {
        self.layout({  $0.left.top.equalToSuperview().inset(inset) })
    }
}

或者,如果您想要使用自己的DSL,则可以使用.set(constraint: AnchorLayoutBuilderConstraint)来设置SnapKit锚点。

extension AnchorLayoutBuilderConvertible {
    func leftTopAnchor(_ constraint: AnchorLayoutBuilderConstraint) -> AnchorLayoutBuilder {
        self.layout({  $0.left.top.set(constraint: constraint) })
    }
}
扩展 DSL

AnchorLayoutBuilderConstraint 协议有 4 个属性

  • 目标
  • inset(可选)
  • comparisonType(等于、小于、大于)
  • 优先级

您可以通过实现 MutableAnchorLayoutBuilderConstraint 来扩展它,添加额外的语法糖,并修改此约束。

为什么我应该选择这个框架?

已经有了很多像 SwiftUI 这样的声明式布局框架,但 DeclarativeLayoutKit 从中脱颖而出

  • 代码库小
    没有大量对象,我并不是 "重新发明轮子"。这不是一个 新的布局系统,而是一套辅助工具:AnchorLayoutBuilderStackingLayoutBuilderViewStyle。这个 README 描述就是您所需了解的所有内容,无需额外的文档。
    💉并且您可以轻松地将此框架集成到项目中,并进行 与旧/现有布局代码的组合

  • 可扩展性
    该框架简单且易于扩展。只需应用 BuilderFactory 模式来添加新功能。

  • 单独的
    该框架解决了 3 个任务

    • 属性链
    • 布局
    • 样式

    您可以 使用特定所需的功能集。更多内容请参见 安装章节

安装

CocoaPods

# Podfile
use_frameworks!

target 'YOUR_TARGET_NAME' do
    pod 'DeclarativeLayoutKit'

    # if you only want property-chaining feature:
    # pod 'DeclarativeLayoutKit/Chaining'

    # if you only want anchor-layouting feature:
    # pod 'DeclarativeLayoutKit/Layouting'

    # if you only want view styling feature:
    # pod 'DeclarativeLayoutKit/Styling'
end

YOUR_TARGET_NAME 替换掉,然后在 Podfile 目录中,输入以下命令

$ pod install

Swift Package Manager

创建一个 Package.swift 文件。

// swift-tools-version:5.0

import PackageDescription

let package = Package(
  name: "YOUR_PROJECT_NAME",
  dependencies: [
      .package(url: "https://github.com/Ernest0-Production/DeclarativeLayoutKit.git", from: "1.0.0")
  ],
  targets: [
      .target(name: "YOUR_TARGET_NAME", dependencies: ["DeclarativeLayoutKit"])
      // if you only want property-chaining feature:
      // dependencies: ["DeclarativeLayoutKit/Chaining"])

      // if you only want anchor-layouting feature:
      // dependencies: ["DeclarativeLayoutKit/Layouting"])

      // if you only want view styling feature:
      // dependencies: ["DeclarativeLayoutKit/Styling])
  ]
)

归功于

授权

DeclarativeLayoutKit 采用 MIT 授权发布。详细信息请参阅LICENSE


待办事项

  • 可替换布局系统(NSLayoutConstraints,SnapKit,基于框架的布局)
  • 可占位符?
  • 可加载?