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

235 阅读5分钟

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], 最终作为parent1subviews加入;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:

  • 偏移

\

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<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"

\

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


var text: String {

    set(newValue) {

        _text.wrappedValue = newValue

    } 

    get {

        _text.wrappedValue

    }

}

var _text: State<String>

\

也就是说,修改text,即修改_textwrappedValue,即触发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),完全不用考虑传StringState<String>还是Binding<String>

\

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

\

LazyFish源码地址

\