DSL, Domain Specific Language, Domain Specific Language

A programming language, Turing-complete, with both functionality and performance. For example, Swift,

DSLS are based on a language and are designed to solve a particular problem. Ideal for declarative, rule-specific scenarios

On this issue, the grammar is concise and easy to handle. Such as SnapKit

DSL, simple to write, improve development efficiency. Establish context domains that hide a lot of implementation details. This code is less, not long. In general, the compile time of your code increases

Swift has the advantages of type derivation function Type refer, protocol-based programming POP and operator overloading, which makes it convenient to develop ITS DSL.

The namespace comes last

This article uses a view layout as an example:

Native layout, using LayoutAnchor

label.translatesAutoresizingMaskIntoConstraints = falseNSLayoutConstraint. Activate ([/ / at the top of the label, from the bottom of the button, 20 pt label. TopAnchor. The constraint (equalTo: Button. BottomAnchor, constant: 20), / / to the left of the label, align the left side of the button label. LeadingAnchor. The constraint (equalTo: Button. LeadingAnchor), / / the width of the label, no more than the width of the button - 40 pt label. WidthAnchor. The constraint (lessThanOrEqualTo: view.widthAnchor, constant: -40 ) ])Copy the code

With the DSL built in this article, there is much less layout code and more intuitive notation

// put {label.put. Layout {$0.top == button.put.bottom + 20
            $0.leading == button.put.leading
            $0.width <= view.put.width - 40
        }
Copy the code

The first step is to encapsulate native layout functionality,LayoutAnchor

The functional protocol LayoutAnchor needs to be established to extract and merge 6 layout methods of iOS system into 3.

NSLayoutAnchor is a generic class. Each specific constraint anchor, along with the specific NSLayoutAnchor class, has its own associated protocol. The implementation details are complicated.

Establish the functional protocol LayoutAnchor to screen out the tedious details


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

Copy the code

Create an upper classLayoutProxyIn the native layout method, wrap one layer. This calls the syntax less

Get the attributes first,

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)
    }
}
Copy the code

Then call the layout method

Encapsulate a layer, change the native method name

Add a structureLayoutProperty, which packages a property anchor that complies with LayoutAnchor. In this way, NSLayoutAnchor can be directly added to NSLayoutAnchor without directly operating NSLayoutAnchor, which is more elegant


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}}Copy the code
Results after the first step:

Call syntax, slightly refined

       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)
Copy the code

Step 2: Use closures to set up the layout context and encapsulate the code that the layout calls

The context tells us what’s going on here. Easy to understand

Manually create a layout object, let proxy = LayoutProxy(View: Label), and then specific layout

Boiler plate, still a little bit more. It’s not very elegant to repeat the same routine every time.

Use Swift’s closure to set up the execution context, which is a bit more DSL

Context, such as SnapKit.

If you see.snp{}, you know what’s going on. Here, only the layout will be relevant, will not do other

Add extension methods to UIView, configure UIView, and execute the closure of LayoutProxy

extension UIView {
    func layout(using closure: (LayoutProxy) -> Void) {
        translatesAutoresizingMaskIntoConstraints = false
        closure(LayoutProxy(view: self))
    }
}
Copy the code
Effect after Step 2: Compare DSLS

Looks like an animation call to 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)
}

Copy the code

Step 3: Operator overloading to further simplify the syntax

Replace the method called in step 2 with an action symbol

Add and subtract, combine constraints and offsets into a tuple

// add func +<A: LayoutAnchor>(LHS: A, RHS: CGFloat) -> (A, CGFloat) {return(LHS, RHS)} // subtract func -<A: LayoutAnchor>(LHS: A, RHS: CGFloat) -> (A, CGFloat) {return (lhs, -rhs)
}
Copy the code
Whether X is offset in the three cases where the constraint is in effect

There are 3 possibilities X there are 2 possibilities

Func ==<A: LayoutAnchor>(LHS: LayoutProperty<A>, RHS: (A, CGFloat)) {lhs.equal(to: RHS.0, offsetBy: RHS.1)} // equal to, use ==, as = func ==<A: LayoutAnchor>(LHS: LayoutProperty<A>, RHS: A) {LHS. Func >=<A: LayoutAnchor>(LHS: LayoutProperty<A>, RHS: LayoutProperty<A>, (A, CGFloat)) {lhs.greaterThanorequal (to: hs.0, offsetBy: HS.1)} // not less than func >=<A: LayoutAnchor>(LHS: LayoutProperty<A>, RHS: A) {lhs.greaterThanoreQual (to: RHS)} // Not greater than, // Right argument with offset func <=<A: LayoutAnchor>(LHS: A) LayoutProperty<A>, RHS: (A, CGFloat)) {lhs.lessthanoreQual (to: rhs.0, offsetBy: rhS.1)} // No greater than func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) { lhs.lessThanOrEqual(to: rhs) }Copy the code
Effect after Step 3: DSL
label.layout {
    $0.top == button.bottomAnchor + 20
    $0.leading == button.leadingAnchor
    $0.width <= view.widthAnchor - 40
}
Copy the code

Step 4: Add the namespace

Namespaces, which may seem very large, actually encapsulate one layer

A namespace can look like this: NamespaceWrapper(val: view)

Encapsulation structure,

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

Copy the code
Add functionality to the structure


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


Copy the code

Call effect looks like this, usually do not see

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

Copy the code

theNamespaceWrapper(val: view)To become common to usview.put

(View layout has the meaning of placing, here we use put)

Put a glue protocol called NamespaceWrap to do this transformation, and UIView obeys that protocol.

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


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

extension UIView: NamespaceWrap{ }

Copy the code
Effect after Step 4: DSL
label.put.layout {
            $0.top == button.put.bottom + 20
            $0.leading == button.put.leading
            $0.width <= view.put.width - 40
        }
Copy the code

Code link