理解下 Swift 命名空间和 DSL 设计,例子是视图布局

1,944 阅读2分钟

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: 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
        }

代码链接