1. 程式人生 > IOS開發 >理解下 Swift 名稱空間和 DSL 設計,例子是檢視佈局

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

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

一門程式語言,圖靈完備,功能有,效能也有。譬如 Swift

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

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

DSL,寫起來簡練,提升開發效率。建立上下文 domain,隱藏大量的實現細節。這樣程式碼少,不冗長。一般,程式碼的編譯時間會增加

Swift 有型別推導功能 type refer、協議化程式設計 POP、操作符過載等優勢,開發其 DSL 比較方便。

名稱空間,放在了最後

本文以檢視佈局 layout 為例子:

原生布局,使用 LayoutAnchor

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 後, 佈局程式碼少了很多,符號更加直觀

// 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 個。

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)
}
複製程式碼
約束生效的三種情況 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>,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>,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
        }
複製程式碼

程式碼連結