LazyFish:简单的UIViewDSL轻量框架介绍
self.view.arrangeViews {
UILabel()
.text("Hello World")
.alignment(.center)
}
起点:
SwiftUI的简洁的表达方式:
SwiftUI使用简洁的表达方式描绘UI布局,是未来的趋势。
虽说iOS13就可以用,但SwiftUI的也在新版本迭代中对iOS13的功能兼容性就不太好,猜测实际可用的版本应该会在iOS14、15以上。
ResultBuilder提供灵活的组件序列生成
Swift5.4提供了自行实现resultBuilder的可能性(在更早版本5.1是命名为functionBuilder的隐藏功能)
Swift官方的ResultBuilder介绍
想到SwiftUI中的View.body
就说ResultBuilder的一种实现,那么能否把UIView和ResultBuilder结合,实现像SwiftUI那样的简洁表达
(暂时忽略Combine框架里@State
、@Binding
等相关的高级功能,涉及到View的刷新)
人家SwiftUI这样写:
VStack {
Text("Hello") //.font(...)
Text("World")
}
那么我用UIView写出类似的表达(假设):
UIStactView(.vertical) {
UILabel("Hello") //.font(...)
UILabel("World")
}
目标
- 支持UIView的声明式布局
- 支持低版本系统例如iOS9
- 默认排版行为不一定与SwiftUI一致
- 能投入使用
ResultBuilder使用
假设我有一个方法,给array
添加元素:
mutating func appendContents(other: [Element]) {
self.append(contentsOf: other) // 这里是原生的添加元素方法
}
// 传入一个数组
array.appendContents([a, b, c, d])
要使用这个方法需要传入一个数组,感觉不太灵活
将other参数改为() -> [Element]类型,并使用ResultBuilder修饰:
mutating func appendContents(@ResultBuilder<Element> other: () -> [Element]) {
let otherArr = other()
self.append(contentsOf: otherArr)
}
// 调用会变成这样!
array.appendContents {
a
b
c
d
}
甚至加上if..else..
、for..in..
:
array.appendContents {
if somevalue == 1 {
a
} else {
b
}
for i in 0..<100 {
c
}
d
}
具体的ResultBuilder能兼容什么写法,取决于实现了哪些BuildBlock函数。
最基本的ResultBuilder需要实现一个buildBlock(...)
方法,以下的实现将所有元素组合成一维数组返回,例如返回[UIView]
、[String]
等。为了方便,将ResultBuilder
写成泛型ResultBuilder<T>
,不局限于UIView
@resultBuilder public struct ResultBuilder<MyReturnType> {
public static func buildBlock(_ components: [MyReturnType]...) -> [MyReturnType] {
let res = components.flatMap { r in
return r
}
return res
}
}
其他复杂功能可酌情添加:
extension ResultBuilder {
// MARK: 处理空白block
static func buildOptional<T>(_ component: [T]?) -> [MyReturnType]
// MARK: 处理不包含else的if语句
static func buildOptional(_ component: [MyReturnType]?) -> [MyReturnType]
// MARK: 处理每一行表达式的返回值
static func buildExpression(_ expression: MyReturnType) -> [MyReturnType]
static func buildExpression(_ expression: MyReturnType?) -> [MyReturnType]
static func buildExpression(_ expression: Void) -> [MyReturnType]
static func buildExpression(_ expression: Void?) -> [MyReturnType]
static func buildExpression(_ expression: [MyReturnType]) -> [MyReturnType]
// MARK: Available API
static func buildLimitedAvailability(_ component: [MyReturnType]) -> [MyReturnType]
// MARK: 处理for循环
static func buildArray(_ components: [[MyReturnType]]) -> [MyReturnType]
// MARK: 处理if...else...(必须包含else)
static func buildEither(first component: [MyReturnType]) -> [MyReturnType]
static func buildEither(second component: [MyReturnType]) -> [MyReturnType]
}
假设要实现以下例子,表达一个视图层级:
parent1 {
child1
child2
child3 {
grandchild1
grandchild2
}
}
child1、child2、child3
这3个元素将被组合为[child1, child2, child3]
, 最终作为parent1
的subviews
加入;grandchild1、2
同理
给UIView拓展几个方法,可以直接加入[UIView]
作为subview
,或者init
时就加入[UIView]
extension UIView {
@discardableResult func arrangeViews(@ResultBuilder<UIView> content: () -> [UIView]) -> Self {
let views = content()
for i in views {
// 如果是self是stackview使用addArrangedSubview
self.addSubview(i)
}
/// 其他细节
return self
}
convenience init(@ResultBuilder<UIView> content: () -> [UIView]) {
self.init()
self.arrangeViews(content)
}
}
// 调用
view.arrangedSubviews {
child1
child2
child3 {
grandchild1
grandchild2
}
...
}
UIView {
child1
child2
child3 {
grandchild1
grandchild2
}
...
}
这样我们就通过ResultBuilder实现了视图层级的创建
布局规则
以上内容已经完成了ResultBuilder的使命,UIView也确实已加入到superview中,但是还没布局,如何布局?
目前已实现的布局规则比较简单:
alignment:
- superview对齐(
top、leading、bottom、trailing、allEdges、centerX、centerY、center
等)
frame:
- 大小等于常量(
width、height = constant
) - 大小等于变量(
width、height = Binding<CGFloat>
)
padding:
- 内边距
offset:
- 偏移
padding
和offset
,比较取巧的用了一个额外的容器去实现(使用offset
时,不确保view
可以被正确点击)
目前暂无subviews
之间的相互约束规则与实现,仅能通过stack
或其他方式进行排列
举个例子
使用举例,展示一个文本和输入框,注意.alignment()、.padding()、.frame()
可以重复改写:
@State var text: String = "abc"
override func viewDidLoad() {
super.viewDidLoad()
self.view.arrangeViews {
UIView() {
UIStackView(axis: .vertical, spacing: 10) {
UILabel().text("your input:")
UILabel().text(binding: self.$text)
UITextField().text(binding: self.$text).borderStyle(.roundedRect)
}
.padding(top: 10, leading: 10, bottom: 10, trailing: 10)
.alignment(.allEdges)
}
.borderWidth(1)
.borderColor(.black)
.frame(width: 200)
.alignment(.top, value: 160)
.alignment(.centerX, value: 0)
}
// Do any additional setup after loading the view.
}
以上就是基本的view声明与排版
关于view、label、button
的一些常用属性修改,也封装成可以链式调用的方法,例如UILabel:
UILabel()
.text("abc")
.textColor(.red)
.font(.systemFont(ofSize: 14, weight: .semibold))
.backgroundColor(.yellow)
.border(width: 1, color: .green)
有需要可自行拓展,确保返回Self
类型即可
如何刷新页面或元素
如果仅是上文提到的内容,那么所有视图都是静态的,很难有改动的可能
参考SwiftUI使用了@State
、@Binding
修饰符,在修改他们的所修饰的属性时,页面就会自动刷新,具体到文本内容、视图是否展示、数量等
那么我也需要实现一个自己的@State
或@Binding
,可以在属性被修改时做某些事
那么@propertyWrapper
就可以很好的实现这个需求,可以参考Swift官方的propertyWrapper介绍
实现@State和Binding?
要支持泛型,且改动时要触发动作,改写didSet
,加上observers
的数组(个人认为用class比较靠谱)
@propertyWrapper public class State<T> {
public var wrappedValue: T {
didSet {
let newValue = wrappedValue
let oldValue = oldValue
for obs in self.observers {
let changed = Changed(old: oldValue, new: newValue)
obs(changed)
}
}
}
public init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
public typealias ObserverHandler = (Changed<T>) -> Void
private var observers = [ObserverHandler]()
public func addObserver(observer: @escaping ObserverHandler) {
self.observers.append(observer)
let changed = Changed(old: wrappedValue, new: wrappedValue)
observer(changed)
}
public struct Changed<T> {
let old: T
let new: T
}
}
这时候我们可以使用@State
修饰属性:
@State var text: String = "abc"
编译器会自动给我们生成一对属性(内部的set
和get
仅为个人猜测):
var text: String {
set(newValue) {
_text.wrappedValue = newValue
}
get {
_text.wrappedValue
}
}
var _text: State<String>
也就是说,修改text
,即修改_text
的wrappedValue
,即触发observers
调用
那么给UILabel添加一个接收这个_text
的方法,就直接可以实现动态修改label
文本了:
extension UILabel {
func text(state stateText: State<String>?) -> Self {
stateText?.addObserver { [weak self] changed in
self?.text = changed.new
}
return self
}
}
// 调用
UILabel().text(state: _text)
个人认为传入_text
不好看,像$text
、#text
?之类的就看起来比较厉害
还真有办法让编译器额外生成一个$text
属性,给State添加projectedValue,暂且命名为Binding类型(此Binding非彼Binding):
extension State {
public var projectedValue: Binding<T> {
return Binding(wrapper: self)
}
}
public struct Binding<T> {
var wrapper: State<T>
}
var text: String
var _text: State<String>
var $text: Binding<String>
修改UILabel:
extension UILabel {
func text(binding bindingText: Binding<String>?) -> Self {
bindingText?.wrapper.addObserver { [weak self] changed in
self?.text = changed.new
}
return self
}
}
// 调用
UILabel().text(binding: $text)
这里和SwiftUI不一样。SwitUI只需Text(text)
,完全不用考虑传String
、State<String>
还是Binding<String>
- 注:源码不断在进化中,可能与文章内容有出入,具体可看源码里的Readme.md