理解下 Swift 命名空間和 DSL 設計,例子是視圖佈局

DSL ,領域專用語言, Domain Specific Languagegit

一門編程語言,圖靈完備,功能有,性能也有。譬如 Swiftgithub

DSL 基於一門語言,專門解決某一個問題。適合聲明式,規則明確的場景編程

該問題上,語法簡練,處理方便。譬如 SnapKitbash

DSL,寫起來簡練,提高開發效率。創建上下文 domain,隱藏大量的實現細節。這樣代碼少,不冗長。通常,代碼的編譯時間會增長

Swift 有類型推導功能 type refer、協議化編程 POP、操做符重載等優點,開發其 DSL 比較方便。閉包

命名空間,放在了最後

本文以視圖佈局 layout 爲例子:

原生布局,使用 LayoutAnchorapp

label.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
    //    label 的頂部,距離  button 的底部, 20 pt
    label.topAnchor.constraint(
        equalTo: button.bottomAnchor,
        constant: 20
    ),  
     //    label 的左邊,對齊  button 的左邊
    label.leadingAnchor.constraint(
        equalTo: button.leadingAnchor
    ),
    //    label 的寬度,不超過  button 的寬度 - 40 pt
    label.widthAnchor.constraint(
        lessThanOrEqualTo: view.widthAnchor,
        constant: -40
    )
])
複製代碼

使用本文造的 DSL 後, 佈局代碼少了不少,符號更加直觀less

// put , 有放置的意思
label.put.layout {
            $0.top == button.put.bottom + 20
            $0.leading == button.put.leading
            $0.width <= view.put.width - 40
        }
複製代碼

第一步,封裝原生的佈局功能, LayoutAnchor

須要創建功能協議 LayoutAnchor, 把 iOS 系統有 6 個佈局方法,抽離合併成 3 個。dom

NSLayoutAnchor 是一個泛型類。每個具體的約束錨點,搭配具體的 NSLayoutAnchor 類,自帶相關的協議。實現細節比較複雜。

創建功能協議 LayoutAnchor,把繁瑣的細節,屏蔽掉編程語言

protocol LayoutAnchor {
    func constraint(equalTo anchor: Self,
                    constant: CGFloat) -> NSLayoutConstraint
    func constraint(greaterThanOrEqualTo anchor: Self,
                    constant: CGFloat) -> NSLayoutConstraint
    func constraint(lessThanOrEqualTo anchor: Self,
                    constant: CGFloat) -> NSLayoutConstraint
}


extension NSLayoutAnchor: LayoutAnchor {}

複製代碼

創建一個上層類 LayoutProxy, 在原生布局方法上,包裹一層。這樣調用語法少一點

先拿到屬性,佈局

class LayoutProxy {
    lazy var leading = property(with: view.leadingAnchor)
    lazy var trailing = property(with: view.trailingAnchor)
    lazy var top = property(with: view.topAnchor)
    lazy var bottom = property(with: view.bottomAnchor)
    lazy var width = property(with: view.widthAnchor)
    lazy var height = property(with: view.heightAnchor)

    private let view: UIView

    fileprivate init(view: UIView) {
        self.view = view
    }

    private func property<A: LayoutAnchor>(with anchor: A) -> LayoutProperty<A> {
        return LayoutProperty(anchor: anchor)
    }
}
複製代碼

再調用佈局方法

封裝一層,把原生的方法名,給改了

增長一個結構體 LayoutProperty, 他包了個遵照 LayoutAnchor 的屬性 anchor. 這樣能夠不用直接操做 NSLayoutAnchor ,直接給 NSLayoutAnchor 增長方法,優雅一些

struct LayoutProperty<Anchor: LayoutAnchor> {
    fileprivate let anchor: Anchor
}

extension LayoutProperty {
    func equal(to otherAnchor: Anchor, offsetBy constant: CGFloat = 0) {
        anchor.constraint(equalTo: otherAnchor,
                          constant: constant).isActive = true
    }

    func greaterThanOrEqual(to otherAnchor: Anchor,
                            offsetBy constant: CGFloat = 0) {
        anchor.constraint(greaterThanOrEqualTo: otherAnchor,
                          constant: constant).isActive = true
    }

    func lessThanOrEqual(to otherAnchor: Anchor,
                         offsetBy constant: CGFloat = 0) {
        anchor.constraint(lessThanOrEqualTo: otherAnchor,
                          constant: constant).isActive = true
    }
}
複製代碼
第一步後的效果:

調用語法,略微精煉

label.translatesAutoresizingMaskIntoConstraints = false

        let proxy = LayoutProxy(view: label)
        proxy.top.equal(to: button.bottomAnchor, offsetBy: 20)
        proxy.leading.equal(to: button.leadingAnchor)
        proxy.width.lessThanOrEqual(to: view.widthAnchor, offsetBy: -40)
複製代碼

第二步: 採用閉包,創建佈局上下文環境, 封裝佈局調用的代碼

上下文環境, 說明了這裏是幹什麼的。方便理解

手動創建佈局對象,let proxy = LayoutProxy(view: label),再具體佈局

薄板代碼 boiler plate,仍是多了一些。每次都要重複這個套路,不怎麼優雅。

採用 Swift 的閉包 closure,創建執行上下文環境,更加 DSL 一些

上下文環境,譬如 SnapKit.

看見 .snp{}, 就知道這裏面是幹什麼的。在這裏,只會佈局相關,不會幹其餘

UIView 添加擴展方法,配置 UIView 後,執行 LayoutProxy 的閉包

extension UIView {
    func layout(using closure: (LayoutProxy) -> Void) {
        translatesAutoresizingMaskIntoConstraints = false
        closure(LayoutProxy(view: self))
    }
}
複製代碼
第 2 步後的效果:比較 DSL 了

看起來像動畫調用 UIView.animate

label.layout {
    $0.top.equal(to: button.bottomAnchor, offsetBy: 20)
    $0.leading.equal(to: button.leadingAnchor)
    $0.width.lessThanOrEqual(to: view.widthAnchor, offsetBy: -40)
}

複製代碼

第 3 步: 操做符重載,進一步簡化語法

將第 2 步的調用方法,用操做符號替換

加和減,把約束和偏移,結合成元組 tuple

// 加
func +<A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> (A, CGFloat) {
    return (lhs, rhs)
}
// 減
func -<A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> (A, CGFloat) {
    return (lhs, -rhs)
}
複製代碼
約束生效的三種狀況 X 要不要偏移

3 種狀況 X 2 種條件

// 等於, 使用  == ,看成 =
// 右邊參數,含偏移
func ==<A: LayoutAnchor>(lhs: LayoutProperty<A>,
                         rhs: (A, CGFloat)) {
    lhs.equal(to: rhs.0, offsetBy: rhs.1)
}

// 等於, 使用  == ,看成 =
func ==<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
    lhs.equal(to: rhs)
}


// 不小於,
// 右邊參數,含偏移
func >=<A: LayoutAnchor>(lhs: LayoutProperty<A>,
                         rhs: (A, CGFloat)) {
    lhs.greaterThanOrEqual(to: rhs.0, offsetBy: rhs.1)
}

// 不小於
func >=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
    lhs.greaterThanOrEqual(to: rhs)
}

// 不大於,
// 右邊參數,含偏移
func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>,
                         rhs: (A, CGFloat)) {
    lhs.lessThanOrEqual(to: rhs.0, offsetBy: rhs.1)
}

// 不大於
func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
    lhs.lessThanOrEqual(to: rhs)
}

複製代碼
第 3 步後的效果: DSL 了
label.layout {
    $0.top == button.bottomAnchor + 20
    $0.leading == button.leadingAnchor
    $0.width <= view.widthAnchor - 40
}
複製代碼

第 4 步: 增長命名空間

命名空間,看起來很高很大,實際上就封裝了一層

命名空間能夠長這個樣子,NamespaceWrapper(val: view)

封裝結構體,
public protocol TypeWrapper{
    associatedtype WrappedType
    var wrapped: WrappedType { get }
    init(val: WrappedType)
}

public struct NamespaceWrapper<T>: TypeWrapper{
    public let wrapped: T
    public init(val: T) {
        self.wrapped = val
    }
}

複製代碼
給結構體添加功能
extension TypeWrapper where WrappedType: UIView {
    func layout(using closure: (LayoutProxy) -> Void) {
        wrapped.translatesAutoresizingMaskIntoConstraints = false
        closure(LayoutProxy(view: wrapped))
    }
    
    var bottom: NSLayoutYAxisAnchor{
        wrapped.bottomAnchor
    }
    
    var leading: NSLayoutXAxisAnchor{
        wrapped.leadingAnchor
    }
    
    
    var width: NSLayoutDimension{
        wrapped.widthAnchor
    }
    
    
    var centerX: NSLayoutXAxisAnchor{
        wrapped.centerXAnchor
    }
    
    var centerY: NSLayoutYAxisAnchor{
        wrapped.centerYAnchor
    }
    
}


複製代碼

調用效果長這樣,日常見不到的

NamespaceWrapper(val: label).layout {
            $0.top == NamespaceWrapper(val: button).bottom + 20
            // ...            
        }
        

複製代碼

NamespaceWrapper(val: view) 變成咱們常見的 view.put

( 視圖佈局有放置的含義,這裏用 put )

弄一膠水協議 NamespaceWrap 完成這個轉換,UIView 遵照這個協議。

public protocol NamespaceWrap{
    associatedtype WrapperType
    var put: WrapperType { get }
}


public extension NamespaceWrap{
    var put: NamespaceWrapper<Self> {
        return NamespaceWrapper(val: self)
    }
}

extension UIView: NamespaceWrap{ }

複製代碼
第 4 步後的效果: DSL
label.put.layout {
            $0.top == button.put.bottom + 20
            $0.leading == button.put.leading
            $0.width <= view.put.width - 40
        }
複製代碼

代碼連接

相關文章
相關標籤/搜索