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)
        }
複製代碼
相關文章
相關標籤/搜索