LazyFish:简单的UIViewDSL轻量框架介绍

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 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,不局限于UIView

@resultBuilder public struct ResultBuilder {
    public static func buildBlock(_ components: [MyReturnType]...) -> [MyReturnType] {
        let res = components.flatMap { r in
            return r
        }
        return res
    }
}

其他复杂功能可酌情添加:

extension ResultBuilder {
    // MARK: 处理空白block
    static func buildOptional(_ 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], 最终作为parent1subviews加入;grandchild1、2同理

给UIView拓展几个方法,可以直接加入[UIView]作为subview,或者init时就加入[UIView]

extension UIView {
    @discardableResult func arrangeViews(@ResultBuilder content: () -> [UIView]) -> Self {
        let views = content()
        for i in views {
            // 如果是self是stackview使用addArrangedSubview
            self.addSubview(i)
        }
        /// 其他细节
        return self
    }

    convenience init(@ResultBuilder 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

padding:

  • 内边距

offset:

  • 偏移

paddingoffset,比较取巧的用了一个额外的容器去实现(使用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 {
    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) -> 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 {
        let old: T
        let new: T
    }
}

这时候我们可以使用@State修饰属性:

@State var text: String = "abc"

编译器会自动给我们生成一对属性(内部的setget仅为个人猜测):

var text: String {
    set(newValue) {
        _text.wrappedValue = newValue
    } 
    get {
        _text.wrappedValue
    }
}
var _text: State

也就是说,修改text,即修改_textwrappedValue,即触发observers调用

那么给UILabel添加一个接收这个_text的方法,就直接可以实现动态修改label文本了:

extension UILabel {
    func text(state stateText: State?) -> 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 {
        return Binding(wrapper: self)
    }
}

public struct Binding {
    var wrapper: State
}
var text: String
var _text: State
var $text: Binding

修改UILabel:

extension UILabel {
    func text(binding bindingText: Binding?) -> Self {
        bindingText?.wrapper.addObserver { [weak self] changed in
            self?.text = changed.new
        }
        return self
    }
}

// 调用
UILabel().text(binding: $text)

这里和SwiftUI不一样。SwitUI只需Text(text),完全不用考虑传StringState还是Binding

  • 注:源码不断在进化中,可能与文章内容有出入,具体可看源码里的Readme.md

LazyFish源码地址

你可能感兴趣的:(LazyFish:简单的UIViewDSL轻量框架介绍)