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)
目前,属性链式调用支持以下类型:UIView
、UIControl
、UILabel
、UIImageView
、UIScrollView
、UITextView
、UITableView
、UICollectionView
、UITextField
、UIButton
、UISlider
、UISwitch
、UIStackView
。
您还可以轻松地为其他类型生成函数 - 查看方法
😋 并且还有一些额外的语法糖
可分配给变量
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
。默认的target
是nil
,与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)
HStack
和VStack
是一个将视图
(或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
}
}
- 第二种方式 - 使用Sourcery 应用Chainable模板并使用您的自定义视图(教程)。
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 从中脱颖而出
-
代码库小
没有大量对象,我并不是 "重新发明轮子"。这不是一个 新的布局系统,而是一套辅助工具:AnchorLayoutBuilder
、StackingLayoutBuilder
、ViewStyle
。这个README
描述就是您所需了解的所有内容,无需额外的文档。
💉 并且您可以轻松地将此框架集成到项目中,并进行 与旧/现有布局代码的组合。 -
可扩展性
该框架简单且易于扩展。只需应用Builder
和Factory
模式来添加新功能。 -
单独的
该框架解决了 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])
]
)
归功于
- Telegram: @Ernest0N
授权
DeclarativeLayoutKit 采用 MIT 授权发布。详细信息请参阅LICENSE。
待办事项
- 可替换布局系统(NSLayoutConstraints,SnapKit,基于框架的布局)
- 可占位符?
- 可加载?