C++ 实现一个 AutoLayout 的 DSL

May 18, 2022 • 预计阅读时间 3 分钟

DSL 全称是 Domain-Specific Language,叫作领域专用语言。用于解决特定问题而提出的编程语言。比如 CSS 就是解决网页中布局问题而产生的 DSL。

自动布局(AutoLayout)是开发中必不可少的,但是其 API 沉长难记而且写出来的代码不直观。为了解决这些而出现了 Masonry 和 SnapKit。这两个库也属于自动布局的 DSL。

虽然已经有了这两个库,但是一个是 ObjC 开发的,另一个是 Swift 开发的。

为了增加趣味性,我来使用 C++ 来实现一个自动布局的 DSL。

苹果出品的自动布局是基于约束关系来计算视图的位置,每条约束都可以表达为下面这个算式:

view1.attr1 <relation> multiplier × view2.attr2 + c

比如要描述 view1view2 的左边,间隔是 10pt

view1.right = view2.left - 10

使用自动布局 API 生成上面的约束就是:

[NSLayoutConstraint constraintWithItem:view1
                             attribute:NSLayoutAttributeRight
                             relatedBy:NSLayoutRelationEqual
                                toItem:view2
                             attribute:NSLayoutAttributeLeft
                            multiplier:1
                              constant:-10];

iOS 9 之后,视图增加一些锚点(Anchor)属性,约束写起来简单了一些:

[view1.rightAnchor constraintEqualToAnchor:view2.leftAnchor constant:-10];

以上就是约束要使用的 API,下面开始设计 DSL。

两个锚点(点)存在三种关系:

  • 相等(==
  • 小于等于(<=
  • 大于等于(>=

两个锚点的距离由 multiplierconstant 共同决定。

当确定两个锚点关系的时候,就可以调用 API 创建约束。

设计一个 C++ 模板类 CLayoutAnchor

template<typename T = NSLayoutAnchor>
class API_AVAILABLE(macos(10.11), ios(9.0), tvos(9.0))
CLayoutAnchor {
public:
    CLayoutAnchor(T *layoutAnchor)
        : m_layoutAnchor(layoutAnchor)
        , m_multiplier(1)
        , m_constant(0) {
    }

    CLayoutAnchor() = delete;

    ~CLayoutAnchor() {
        m_layoutAnchor = nil;
    }

private:
    T *m_layoutAnchor;
    CGFloat m_multiplier;
    CGFloat m_constant;
};

typedef CLayoutAnchor<NSLayoutXAxisAnchor> CLayoutXAxisAnchor;
typedef CLayoutAnchor<NSLayoutYAxisAnchor> CLayoutYAxisAnchor;
typedef CLayoutAnchor<NSLayoutDimension> CLayoutDimension;

布局有三种属性:X 轴、Y 轴和尺寸,它们有相似的逻辑只是类型不一样,所以最合适使用模板。

通过重载运算符 ==>=<= 来创建约束:

template<typename _T = T>
NSLayoutConstraint * operator<= (const CLayoutAnchor<T> &rhs) {
    return [m_layoutAnchor constraintLessThanOrEqualToAnchor:rhs.m_layoutAnchor constant:rhs.m_constant];
}

template<typename _T = T>
NSLayoutConstraint * operator>= (const CLayoutAnchor<T> &rhs) {
    return [m_layoutAnchor constraintGreaterThanOrEqualToAnchor:rhs.m_layoutAnchor constant:rhs.m_constant];
}

template<typename _T = T>
NSLayoutConstraint * operator== (const CLayoutAnchor<T> &rhs) {
    return [m_layoutAnchor constraintEqualToAnchor:rhs.m_layoutAnchor constant:rhs.m_constant];
}

template<>
NSLayoutConstraint * operator<= <NSLayoutDimension>(const CLayoutAnchor<T> &rhs) {
    return [m_layoutAnchor constraintLessThanOrEqualToAnchor:rhs.m_layoutAnchor multiplier:rhs.m_multiplier constant:rhs.m_constant];
}

template<>
NSLayoutConstraint * operator>= <NSLayoutDimension>(const CLayoutAnchor<T> &rhs) {
    return [m_layoutAnchor constraintGreaterThanOrEqualToAnchor:rhs.m_layoutAnchor multiplier:rhs.m_multiplier constant:rhs.m_constant];
}

template<>
NSLayoutConstraint * operator== <NSLayoutDimension>(const CLayoutAnchor<T> &rhs) {
    return [m_layoutAnchor constraintEqualToAnchor:rhs.m_layoutAnchor multiplier:rhs.m_multiplier constant:rhs.m_constant];
}

因为只有 NSLayoutDimension 类型才需要 multiplier ,所以使用了类模板函数来限定类型。

再重载运算符 */+- 来实现 multiplier 的乘除与 constant 的加减:

template<typename _T = T>
CLayoutAnchor & operator* (CGFloat m) {
    return *this;
}

template<typename _T = T>
CLayoutAnchor & operator/ (CGFloat m) {
    return *this;
}

template<>
CLayoutAnchor & operator* <NSLayoutDimension>(CGFloat m) {
    m_multiplier *= m;

    return *this;
}

template<>
CLayoutAnchor & operator/ <NSLayoutDimension>(CGFloat m) {
    m_multiplier /= m;

    return *this;
}

CLayoutAnchor & operator+ (CGFloat c) {
    m_constant += c;

    return *this;
}

CLayoutAnchor & operator- (CGFloat c) {
    m_constant -= c;

    return *this;
}

因为只有 NSLayoutDimension 类型才有 multiplier 的计算,所以使用了类模板函数来限定类型。

最后,扩展 UIViewUILayoutGuide,增加属性:

@interface UIView (AutoLayoutDSL)

@property(nonatomic, readonly) CLayoutXAxisAnchor leading;
@property(nonatomic, readonly) CLayoutXAxisAnchor trailing;
@property(nonatomic, readonly) CLayoutYAxisAnchor top;
@property(nonatomic, readonly) CLayoutXAxisAnchor left;
@property(nonatomic, readonly) CLayoutYAxisAnchor bottom;
@property(nonatomic, readonly) CLayoutXAxisAnchor right;

@property(nonatomic, readonly) CLayoutXAxisAnchor centerX;
@property(nonatomic, readonly) CLayoutYAxisAnchor centerY;

@property(nonatomic, readonly) CLayoutDimension width;
@property(nonatomic, readonly) CLayoutDimension height;

@end
@interface UILayoutGuide (AutoLayoutDSL)

@property(nonatomic, readonly) CLayoutXAxisAnchor leading;
@property(nonatomic, readonly) CLayoutXAxisAnchor trailing;
@property(nonatomic, readonly) CLayoutYAxisAnchor top;
@property(nonatomic, readonly) CLayoutXAxisAnchor left;
@property(nonatomic, readonly) CLayoutYAxisAnchor bottom;
@property(nonatomic, readonly) CLayoutXAxisAnchor right;

@property(nonatomic, readonly) CLayoutXAxisAnchor centerX;
@property(nonatomic, readonly) CLayoutYAxisAnchor centerY;

@property(nonatomic, readonly) CLayoutDimension width;
@property(nonatomic, readonly) CLayoutDimension height;

@end

一个简单的 DSL 就完成了,使用起来是这样的:

[NSLayoutConstraint activateConstraints:@[
    view1.right == view2.left - 10
]];

约束的创建已经通过运算符隐藏起来了,使用的时候只需要关注约束关系,减少了心智负担。

为了更有趣味和把 C++ 贯彻到底,使用 std::initializer_list 把上面的方法调用隐藏起来:

static inline void activateConstraints(std::initializer_list<NSLayoutConstraint *> list) API_AVAILABLE(macos(10.11), ios(9.0), tvos(9.0)) {
    std::for_each(list.begin(), list.end(), [](auto item){
        item.active = YES;
    });
}

最终的 DSL 使用方式:

activateConstraints({
    view1.right == view2.left - 10
});

完整代码和示例:

https://github.com/cntrump/CAutoLayoutDSL

DSLAutoLayoutiOS
版权声明:如果转发请带上本文链接和注明来源。

lvv.me

iOS/macOS Developer

Swift 中的值类型和引用类型

在 FreeBSD 中安装 Bash Shell