灵感来源
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 的新功能,具体的功能其他人已经说得很多了,不必赘述!
实现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个视图
如果再多,可以使用Group来处理
对于视图来说,10个或许足够了,但对约束来说,10个远远不够,1个子视图的约束至少是3~4条,多了可能7~8条,一个视图可能添加N个子视图,那约束数量可能是N倍。
好在,约束的类型就一种NSLayoutConstraint我们并不需要如此复杂的泛型条件,而且Swift中也允许任意数量参数的方法定义,我们完全可以设计成如下这样:
@_functionBuilder public struct LayoutBuilder {
public static func buildBlock(_ constraints: NSLayoutConstraint?...) -> [NSLayoutConstraint] {
return constraints.compactMap { $0 }
}
}
现在已经可以简单的使用了, 操作符约束库参见之前的文章(未完成)
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)
}