Swift 5.1 用DSL方式来写约束布局(@functionBuilder采坑记)

灵感来源

SwiftUI 的 DSL 语法让咱们眼前一亮.数组

VStack {
            Text("234124")
            if isAdd {
                Text("3333")
                Text("3333")
            } else {
                
                Text("3333")
                Text("3333")
            }
            Text("234124")
        }
复制代码

结合以前用运算符作约束的代码,彻底可使用DSL方式改进,废话很少说,直接动手闭包

扩展UIView添加 DSL 闭包方法

extension UIView {

    public func layoutConstraints(@LayoutBuilder _ layouts: () -> [NSLayoutConstraint]) {
        addConstraints(layouts())
    }
    
}

复制代码

上面参数名前面的 @LayoutBuilder 是什么呢?它是咱们定一个 约束构造器的结构体编辑器

@_functionBuilder public struct LayoutBuilder {
}
复制代码

其中@_functionBuilder就是 Swift 5.1 的新功能,具体的功能其余人已经说得不少了,没必要赘述!ide

实现LayoutBuilder

参考 SwiftUI 中的 ViewBuilder函数

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@_functionBuilder public struct ViewBuilder {

    /// Builds an empty view from an block containing no statements, `{ }`.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through
    /// unmodified.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    /// Provides support for "if" statements in multi-statement closures, producing an `Optional` view
    /// that is visible only when the `if` condition evaluates `true`.
    public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View

    /// Provides support for "if" statements in multi-statement closures, producing
    /// ConditionalContent for the "then" branch.
    public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View

    /// Provides support for "if-else" statements in multi-statement closures, producing
    /// ConditionalContent for the "else" branch.
    public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
}

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
}

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View
}
......(略)
复制代码

能够看到,官方主要是依赖多态形式实现buildBlock函数,最多从C0 ... C9,容许10个视图post

若是再多,可使用Group来处理测试

对于视图来讲,10个或许足够了,但对约束来讲,10个远远不够,1个子视图的约束至少是3~4条,多了可能7~8条,一个视图可能添加N个子视图,那约束数量多是N倍。ui

好在,约束的类型就一种NSLayoutConstraint咱们并不须要如此复杂的泛型条件,并且Swift中也容许任意数量参数的方法定义,咱们彻底能够设计成以下这样:lua

@_functionBuilder public struct LayoutBuilder {
    public static func buildBlock(_ constraints: NSLayoutConstraint?...) -> [NSLayoutConstraint] {
        return constraints.compactMap { $0 }
    }
}
复制代码

如今已经能够简单的使用了, 操做符约束库参见以前的文章(未完成)spa

view.addSubview(button)
        view.layoutConstraints {
            button.anchor.centerX == view.anchor.centerX
            button.anchor.top == view.anchor.top + 100
        }
复制代码

添加 if else else if等流程控制符的支持

一开始,我觉得很是简单,模仿ViewBuilder中实现buildIf和两个buildEither就足够了

extension LayoutBuilder {
    
    /// Provides support for "if" statements in multi-statement closures, producing an `Optional` view
    /// that is visible only when the `if` condition evaluates `true`.
    public static func buildIf(_ content: NSLayoutConstraint?) -> NSLayoutConstraint? {
        return content
    }
    
    /// Provides support for "if" statements in multi-statement closures, producing
    /// NSLayoutConstraint for the "then" branch.
    public static func buildEither(first: NSLayoutConstraint) -> NSLayoutConstraint {
        return first
    }
    
    /// Provides support for "if-else" statements in multi-statement closures, producing
    /// NSLayoutConstraint for the "else" branch.
    public static func buildEither(second: NSLayoutConstraint) -> NSLayoutConstraint {
        return second
    }
}
复制代码

怀着兴奋,激动的神情试试吧

view.addSubview(button)
        view.layoutConstraints {
            button.anchor.centerX == view.anchor.centerX
            button.anchor.top == view.anchor.top + 100
            if iPhoneX {
                button.anchor.height == 45
            } else {
                button.anchor.height == 40
            }
        }
复制代码

结果居然是失败的!!

XCode提示 [NSLayoutConstraint]没法转换成NSLayoutConstraint

参考其余文章仔细分析缘由,别人说的@_functionBuilder的原理是编辑器将上面的方法翻译成

view.addSubview(button)
        view.layoutConstraints {
            let a = button.anchor.centerX == view.anchor.centerX
            let b = button.anchor.top == view.anchor.top + 100
            let c
            if iPhoneX {
                c = LayoutBuilder.buildEither(button.anchor.height == 45)
            } else {
                c = LayoutBuilder.buildEither(button.anchor.height == 40)
            }
            return LayoutBuilder.buildBlock(a,b,c)
        }
复制代码

若是按照上面的形式,咱们的代码彻底没问题!

但转念一想,也不对,不管是if分支,仍是else分支,均可以写多条约束条件,而不只仅是一条,而若是是任意多条的状况下,显然就不对了

所以,不妨大胆猜想一下,真实的翻译应该是以下

view.addSubview(button)
        view.layoutConstraints {
            let a = button.anchor.centerX == view.anchor.centerX
            let b = button.anchor.top == view.anchor.top + 100
            let c
            if iPhoneX {
                let d = button.anchor.height == 45
                let e = LayoutBuilder.buildBlock(d)
                c = LayoutBuilder.buildEither(e)
            } else {
                let d = button.anchor.height == 40
                let e = LayoutBuilder.buildBlock(d)
                c = LayoutBuilder.buildEither(e)
            }
            return LayoutBuilder.buildBlock(a,b,c)
        }
复制代码

这里的e调用的确定是buildBlock而获得一个约束的数组,而不是单个约束,这样一个控制分支中才能够添加多个约束,所以咱们上面的代码确定要改为

extension LayoutBuilder {
    
    /// Provides support for "if" statements in multi-statement closures, producing an `Optional` view
    /// that is visible only when the `if` condition evaluates `true`.
    public static func buildIf(_ content: [NSLayoutConstraint]?) -> [NSLayoutConstraint]? {
        return content
    }
    
    /// Provides support for "if" statements in multi-statement closures, producing
    /// [NSLayoutConstraint] for the "then" branch.
    public static func buildEither(first: [NSLayoutConstraint]) -> [NSLayoutConstraint] {
        return first
    }
    
    /// Provides support for "if-else" statements in multi-statement closures, producing
    /// [NSLayoutConstraint] for the "else" branch.
    public static func buildEither(second: [NSLayoutConstraint]) -> [NSLayoutConstraint] {
        return second
    }
}
复制代码

到这里又遇到一个难题,最后return的时候,return LayoutBuilder.buildBlock(a,b,c),由于 a,b都是单条约束,而c是约束数组,考虑到闭包中 if else等流程控制语句会很随机的出现,那么buildBlock任意参数的方法就会随机接受到两种类型的参数,这没法经过多态的方式解决,因此咱们要使用一些奇技淫巧。

首先定一个协议,表示约束的元素(单个,或数组)

public protocol LayoutConstraintElements {
    var list:[NSLayoutConstraint] { get }
}
复制代码

而后分别让 单个约束和数组都符合此协议

extension NSLayoutConstraint: LayoutConstraintElements {
    public var list:[NSLayoutConstraint] { return [self] }
}

extension Array : LayoutConstraintElements where Element : NSLayoutConstraint {
    public var list:[NSLayoutConstraint] { return self }
}
复制代码

最后修改LayoutBuilder的代码

@_functionBuilder public struct LayoutBuilder {
    public static func buildBlock(_ constraints: LayoutConstraintElements?...) -> [NSLayoutConstraint] {
        return constraints
            .compactMap { $0?.list }
            .reduce([], +)
    }
}
复制代码

最后测试,OK 大功告成


总结

@functionBuilder 中的 if 等流程控制内容是能够任意多条的

view.addSubview(button)
        view.layoutConstraints {
            let a = button.anchor.centerX == view.anchor.centerX
            let b = button.anchor.top == view.anchor.top + 100
            let c
            if iPhoneX {
                let d = button.anchor.height == 45 && 750
                let e = button.anchor.height <= 50
                let f = button.anchor.height >= 40
                let g = LayoutBuilder.buildBlock(d,e,f)
                c = LayoutBuilder.buildIf(g)
            }
            return LayoutBuilder.buildBlock(a,b,c)
        }
复制代码
相关文章
相关标签/搜索