Puyopuyo
介绍
YouTube: https://youtu.be/3MOBCtIfRFA
哔哩哔哩: https://www.bilibili.com/video/BV1Kh411J7vJ
沟通
Telegram: https://t.me/swift_puyopuyo
QQ: 830599565
描述
一个基于数据驱动的 UIKit 布局库,用 Swift 编写。
要求
swift 5.1
安装
pod 'Puyopuyo'
布局开销
LinearLayout
- | 5 | 10 | 30 | 50 | 80 | 100 | 120 | 150 | 180 | 200 |
---|---|---|---|---|---|---|---|---|---|---|
Puyopuyo | 0.133 | 0.2081 | 0.5259 | 0.7739 | 1.141 | 1.3921 | 1.6517 | 2.0799 | 2.4759 | 2.7499 |
Yoga | 0.1549 | 0.272 | 0.678 | 1.0211 | 1.4729 | 1.8479 | 2.202 | 2.7499 | 3.2939 | 3.664 |
TangramKit | 0.1859 | 0.3151 | 0.7939 | 1.1901 | 1.7089 | 2.1331 | 2.5599 | 3.201 | 3.8499 | 4.2488 |
UIStackView | 0.5679 | 0.699 | 1.7571 | 2.7871 | 5.0063 | 6.901 | 9.392 | 13.741 | 19.3428 | 23.3399 |
FlowLayout
- | 3 | 5 | 10 | 50 | 80 | 100 | 120 | 150 | 180 | 200 |
---|---|---|---|---|---|---|---|---|---|---|
Puyopuyo | 0.1299 | 0.1292 | 0.2298 | 0.9217 | 1.398 | 1.7228 | 2.0749 | 2.5968 | 3.1189 | 3.4549 |
Yoga | 0.1039 | 0.1449 | 0.262 | 1.132 | 1.8072 | 2.2501 | 2.7101 | 3.39 | 4.0788 | 4.549 |
TangramKit | 0.1339 | 0.123 | 0.209 | 0.9109 | 1.4457 | 1.7881 | 2.1469 | 2.675 | 3.2138 | 3.5629 |
视图深度
- | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 |
---|---|---|---|---|---|---|---|---|---|---|
Puyopuyo | 1.0719 | 1.5108 | 1.7547 | 1.6169 | 1.4448 | 1.739 | 2.0349 | 2.2962 | 2.671 | 2.8882 |
Yoga | 2.3851 | 4.7698 | 6.6361 | 8.4819 | 11.2879 | 16.4551 | 23.0069 | 31.568 | 42.9677 | 55.4869 |
TangramKit | 1.7189 | 2.711 | 2.932 | 3.117 | 3.7751 | 4.508 | 5.2759 | 6.1821 | 6.8421 | 7.6298 |
使用方法
一个简单的单元格可以按照以下方式实现。子视图将按照特定规则布局。内置盒子遵循 FlexBox 规则。
/**
VBox HBox
|-----------| |-------------------|
|Title | |Title Description|
| | |-------------------|
|Description|
|-----------|
*/
VBox().attach {
UILabel().attach($0)
.text("Title")
.fontSize(20, weight: .bold)
UILabel().attach($0)
.text("Description")
}
.space(20)
// or
/**
VFlow HFlow
|-------| |-------|
|0 1 2| |0 3 6|
|3 4 5| |1 4 7|
|6 7 | |2 5 |
|-------| |-------|
*/
VFlow(count: 3).attach {
for i in 0..<8 {
UILabel().attach($0)
.text(i.description)
}
}
.space(20)
库提供了三种内置布局
LinearBox
,FlowBox
,ZBox
BoxView
|-- ZBox
|-- LinearBox
|-- HBox
|-- VBox
|-- FlowBox
|-- HFlow
|-- VFlow
布局的核心是视图节点的描述。Measure
和Regulator
测量属性
属性 | 描述 | 值 |
---|---|---|
margin | 当前视图的边距 | UIEdgeInset ,默认:.zero |
alignment | 在父视图中的对齐方式 | .none, .left, .top, .bottom, .vertCenter, .horzCenter ,默认:.none |
size | 尺寸描述 | 尺寸描述 .fixed :固定大小.wrap :包裹内容.ratio 填充剩余空间的比率,.aspectRatio :宽度/高度默认: .wrap ,示例:尺寸属性 |
flowEnding | 仅在FlowBox 中使用,当arranceCount = 0 时,表示当前视图是行中的最后一个视图 |
Bool ,默认:false |
activated | 将由父盒子计算 | Bool ,默认:true |
调节器 & ZBox 属性
ZBox
扩展了Regulator
属性 | 描述 | 值 |
---|---|---|
justifyContent | 控制所有子视图的对齐方式,如果子视图已设置对齐值,则将被对齐值覆盖 | .left, .top, .bottom, .vertCenter, .horzCenter ,默认:.none |
padding | 当前盒子的填充 | UIEdgeInset ,默认:.zero |
线性调节器属性
LinearRegulator
扩展了Regulator
属性 | 描述 | 值 |
---|---|---|
空格 | 控制子视图之间的空间 | CGFloat , 默认: 0 |
格式 | 控制子视图的主轴对齐方式 | .leading, .center, .between, .round, .trailing , 默认: .leading |
反转 | 如果反转添加顺序 | Bool 默认: false |
FlowRegulator 属性
FlowRegulator
扩展 LinearRegulator
属性 | 描述 | 值 |
---|---|---|
排列 | 控制每行中的排列数量,当 arrange = 0 时,将根据内容分隔 |
Int , 默认: 0 |
项目间距 | 项目之间的空间 | CGFloat , 默认: 0 |
行距 | 行之间的空间 | CGFloat , 默认: 0 |
格式 | 每行的格式 | .leading, .center, .between, .round, .trailing , 默认: .leading |
行格式 | 行的格式 | .leading, .center, .between, .round, .trailing , 默认: .leading |
行尺寸 | 运行方向的行尺寸 | (Int) -> SizeDescription , 默认: .wrap(shrink: 1) |
数据驱动
库提供数据驱动 API,以保持你的 UI 始终代表正确的数据。并在有变化时重新布局。
let text = State("")
VBox().attach {
UILabel().attach($0)
.text("Title")
UILabel().attach($0)
.text(text)
}
// do when some data come back
text.value = "My Description"
// if you are using RxSwift that would be another good choise
// make `Observable` implements Outputing protocol
extension Observable: Outputing {
public typealias OutputType = Element
/// .... some code
}
func getDescription() -> Observable<String> {
// get some description by network or other async works
}
// use in view declare
UILabel().attach($0)
.text(getDescription())
PS:数据驱动 API 遵循 RxSwift 的逻辑,请注意内存泄漏。
动画
UIView
具有扩展属性,是 protocol Animator {}
的实例
BoxView 布局后,每个子视图的 center
、bounds
将被分配,如果需要,方块将创建动画。
PS:如果子视图没有动画,将使用最近的父视图的动画,父视图必须是 BoxView 的实例
public struct ExpandAnimator: Animator {
public init(duration: TimeInterval = 0.3) {
self.duration = duration
}
public var duration: TimeInterval
public func animate(_ view: UIView, size: CGSize, center: CGPoint, animations: @escaping () -> Void) {
let realSize = view.bounds.size
let realCenter = view.center
if realSize != size || realCenter != center {
if realSize == .zero, realCenter == .zero {
runAsNoneAnimation {
view.center = center
let scale: CGFloat = 0.5
view.bounds.size = CGSize(width: size.width * scale, height: size.height * scale)
view.layer.transform = CATransform3DMakeRotation(.pi / 8 + .pi, 0, 0, 1)
}
}
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 2, initialSpringVelocity: 5, options: [.curveEaseOut, .overrideInheritedOptions, .overrideInheritedDuration], animations: {
animations()
if realSize == .zero, realCenter == .zero {
view.layer.transform = CATransform3DIdentity
}
}, completion: nil)
} else {
animations()
}
}
}
示例:动画
自定义视图
因为 BoxView 也是一个视图,你可以随意从 Box 继承,以减少视图的层次结构
class MyView: VBox {
override func buildBody() {
// do your view declare here
}
}
状态和事件
复杂数据视图也需要提供数据,创建一些事件,例如点击事件。在UIKit中,也称为DataSource
和Delegate
库提供了定义数据和发出事件的代码模式。
class MyView: VBox, Stateful, Eventable {
// declare your dataSource you need
struct ViewState {
var name: String?
var title: String?
}
// declare event that will occured
enum Event {
case onConfirmed
}
/// declare in Stateful implement by this class
let state = State(ViewState())
/// declare in Eventable implement by this class
let emitter = SimpleIO<Event>()
override func buildBody() {
attach {
UILabel().attach($0)
.text(state.map(\.name)) // use map to transform value
UILabel().attach($0)
.text(binder.title) // use binder dynamic member lookup to find value
UIButton().attach($0)
.text("Confirm")
.bind(event: .touchUpInside, input: emitter.asInput { _ in .onConfirmed })
}
}
}
// Use view
let state = State(MyView.ViewState())
MyView().attach($0)
.state(state) // bind your data source
.onEvent(Inputs { event in
// do your logic when event occured
})
样式
示例:样式
// declare style
let styles: [Style] = [
(\UIView.backgroundColor).getStyle(with: .white),
TapRippleStyle(),
(\UILabel.text).getStyle(with: "Click"),
(\UIView.isUserInteractionEnabled).getStyle(with: true)
]
// Use style
UILabel().attach()
.styles(styles)
扩展
查看 Puyo+xxx.swift
您可以根据需要进一步扩展,期待您的贡献
作者
Jrwong, [email protected]
许可
Puyopuyo 可在 MIT 许可下使用。有关更多信息,请参阅 LICENSE 文件。