理解下 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
}
複製程式碼