DSL 全称是 Domain-Specific Language,叫作领域专用语言。用于解决特定问题而提出的编程语言。比如 CSS 就是解决网页中布局问题而产生的 DSL。
自动布局(AutoLayout)是开发中必不可少的,但是其 API 沉长难记而且写出来的代码不直观。为了解决这些而出现了 Masonry 和 SnapKit。这两个库也属于自动布局的 DSL。
虽然已经有了这两个库,但是一个是 ObjC 开发的,另一个是 Swift 开发的。
为了增加趣味性,我来使用 C++ 来实现一个自动布局的 DSL。
苹果出品的自动布局是基于约束关系来计算视图的位置,每条约束都可以表达为下面这个算式:
view1.attr1 <relation> multiplier × view2.attr2 + c
比如要描述 view1
在 view2
的左边,间隔是 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。
两个锚点(点)存在三种关系:
- 相等(
==
) - 小于等于(
<=
) - 大于等于(
>=
)
两个锚点的距离由 multiplier
和 constant
共同决定。
当确定两个锚点关系的时候,就可以调用 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
的计算,所以使用了类模板函数来限定类型。
最后,扩展 UIView
和 UILayoutGuide
,增加属性:
@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
});
完整代码和示例: