理解下 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
        }
复制代码

代码连接

相关文章
相关标签/搜索