Masonry读后感

iOS 布局

Frame & Bounds

Frame定义:
表示一个View的位置和大小,使用其父视图坐标系。

Bounds定义:
表示一个View的位置和大小,使用其自身的坐标系。

注意点:

  1. 在其父视图坐标器中最小的矩形框。(当view的transform变更时), 这就意味着如果一个视图发生了形变,我们不应该再使用frame的值了。
    官方文档

  2. 当我们修改transform属性的时,所有的变化都是基于视图view的center进行的。谈到center,能影响到的就是anchorPoint。这里是官方描述

  3. 关于transfrom这里也提供一个链接

什么时候使用Bounds & 什么时候使用Frame ?

frame: 由于frame反应了一个view在其父视图的位置,所以当设置一些外部变化的时候使用,比如设置宽高,距离父视图或某个视图的距离。

bounds: view内在的一些变化需要使用bounds,比如:布局其子视图、绘制一些元素。另外当view发生形变之后获取宽高也用bounds。

Demo演示:

基于View实现ScrollView:

AutoLayout非常好,但是官方的API不容易使用, Masonry 解决了这个;

Masonry 简单类图

Masonry.png

Masonry最简单的使用:

1
2
3
4
5
[self.aView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view)
make.top.equalTo(self.view.mas_top).offset(100)
make.width.height.mas_equalTo(200)
}];

从这里 mas_makeConstraints 开始

MAS_VIEW 添加约束有三种方式:

1
2
3
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

通过方法名,很容易知道这几个方法的差异

  1. 添加约束.
  2. 更新约束.
  3. 重新设置约束.
1
2
3
4
5
6
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

line 1: 设置translatesAutoresizingMaskIntoConstraints = NO

  • YES: 系统会将一些属性例如Frame转换成constraints, Default Vlaue in Code View
  • NO: IB Default Value

line 2: 使用当前view实例化MASConstraintMaker

line 3: 执行传进来block,配置maker

注意: 很多人有个疑惑,当使用Masonry时,我们经常在block中直接的引用self,为什么没有形成循环引用?

通过源码可以看出:

  1. 首先,block强引用了self
  2. 假如我们在VC中使用上面的代码, self = VC , block -> VC -> view ;
  3. 这个block没有任何对象持有,所以执行完以后就会销毁。

MASConstraintMaker

先看初始化方法:

1
2
3
4
5
6
7
8
9
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;

self.view = view;
self.constraints = NSMutableArray.new;

return self;
}

MAS_VIEW宏使用,为了框架跨平台 iOS & tvOS & macOS

1
2
3
4
5
#if TARGET_OS_IPHONE || TARGET_OS_TV
#define MAS_VIEW UIView
#elif TARGET_OS_MAC
#define MAS_VIEW NSView
#endif

line 1: Maker对象引用(weak)了MAS_VIEW, 因为view如果不存在了,maker存在也没有意义,所以没有必要进行(strong)引用。

line 2: 实例化约束数组,存储了所有加到当前视图上的约束对象。

make.left调用发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
...
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}

最终,都会调用到- constraint: addConstraintWithLayoutAttribute:里,这里删除了跟make.left无关的代码,后面会提到。

line 1: 设置初始化的MASViewConstraint的代理对象为self
line 2: 集合中添加这根约束
line 3: 返回Constraint对象

到此为止:make.left 这个方法调用执行完毕,生成了一个MASConstraint对象。

MASConstraint

声明:

1
2
// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;

make.left.equalTo(self.view)调用时发生了什么?

乍一看,这个方法很奇怪,返回了一个block对象,这个block接受一个id类型参数并且返回MASConstraint类型对象。我们知道OC调用方法不能使用点语法(属性getter除外),事实上,可以把equalTo看成一个block类型的属性,这个就是属性的getter方法。

实现如下:

1
2
3
4
5
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}

我们直接进入- equalToWithRelation方法中,发现实现体重只有MASMethodNotImplemented(), 证明该方法是抽象想的在MASConstraint中,搜索发现有两个子类:

  • MASViewConstraint
  • MASCompositeConstraint

注意:抽象方法的实现很有意思,在OC中并没有关于抽象方法的定义,如果运行时直接调用到了MASConstraint,这里采用了抛出一个异常的方式,这会引起crash,这种实现抽象类的方式值得学习。

  • 方法名决定了 NSLayoutRelation
  • 参数决定attr

MASViewConstraint

先看简单MASViewConstraint对于equalToWithRelation实现:

1
2
3
4
5
6
7
8
9
10
11
12
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
...
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}

我们这里就equalTo来看,直接进入else逻辑:

line 1:断言判断这里判断关系是否重复建立。这里有环境断言的坑
line 2:设置关系
line 3: 设置约束

事实上,者不难能理解,一个约束(除了size)需要包含3个东西,first ViewAttribute, secondViewAttribute, Layout Relation.

setter 方法中干的什么事情:

1
2
3
4
5
6
7
8
9
10
11
- (void)setSecondViewAttribute:(id)secondViewAttribute {
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
[self setLayoutConstantWithValue:secondViewAttribute];
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
_secondViewAttribute = secondViewAttribute;
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}

启发?

为了对传入值灵活配置,我们可以将复杂的逻辑收敛起来。

这三种类型分别可以通过以下3条语句映射:

1
2
3
make.width.mas_equalTo(100)
make.left.equalTo(self.view)
make.left.equalTo(self.view.mas_left)

可以从setSecondViewAttribute中看出来,line 2 & line3 基本上是等价的;
line 2 就是一个传入了MASViewAttribute,一个传入了MAS_VIEW并内部生成了一个MASViewAttribute,当然这里MAS_VIEW对应的 layoutAttribute 则采用用是firstView的。
line 3 直接传入的是MASViewAttribute,可以直接被持有。

MASViewAttribute

MASViewAttribute 干了三件事:

  • 持有了MAS_VIEW对象
  • id类型的item
  • 持有了NSLayoutAttribute对象

MASViewAttribute提供了两个实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
return self;
}

- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
self = [super init];
if (!self) return nil;

_view = view;
_item = item;
_layoutAttribute = layoutAttribute;

return self;
}

通常情况下,item同view指向相同。当使用 VC-related interface 指向ID < UILayoutSupport>

Install

当maker配置完成之后,就轮到了install了,intall源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)install {
...
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
...
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
...
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
self.installedView = closestCommonSuperview;
...
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}

这里主要的逻辑是:

  1. 根据两个viewAttribute使用系统API生成了一个MASLayoutConstraint对象。
  2. 调用 mas_closestCommonSuperview 方法找到最近的共同的父视图。
  3. 将constraint添加到这个视图上。
  4. 一些记录操作,self持有了layoutConstraint, firstLayoutItem的mas_installedConstraints集合里添加constraint

其中:mas_closestCommonSuperview 实现可以看下;

当然,这里有很多判断,这里只讨论了最通用的流程,到此为止,添加约束的逻辑完成了。

MASCompositeConstraint

倒回去看,在分析 make.left 执行的过程中,忽略了一部分关于 MASCompositeConstraint 代码,这里接着分析下:

在一开始的例子中:make.width.height.mas_equalTo(200); 会执行到这部分逻辑里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) {
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
...
return newConstraint;
}

当我们执行 make.width&left&right&top&bottom等等,会生成一个MASViewAttribute对象,当我们紧接着调用height的时候,会生成一个MASCompositeConstraint对象(这个对象持有了MASConstraint-type数组),然后使用CompoeConstraint替换original constrConstraint;
Masonry正是用这个对象去支持同时设置很多个constraint

有人会问,上面的判断貌似只能对第二次设置有效,make.top.left.bottom.right.equalTo(self.view); 像这样make.top.left 返回了MASCompositeConstraint,再继续调用bottom,就完全在MASCompositeConstraint内部处理了:

1
2
3
4
5
6
7
- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
id<MASConstraintDelegate> strongDelegate = self.delegate;
MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
newConstraint.delegate = self;
[self.childConstraints addObject:newConstraint];
return newConstraint;
}

在这里自己作为自己的代理,MASCompositeConstraint在第二次生成了 - constraint: addConstraint With LayoutAttribute: method.中添加了新增的约束,其中新增的约束是通过下面的代码实现的

1
2
3
4
5
6
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
...
return newConstraint;
}

总结

Masonry 的代码量不大,因为逻辑复用比较多,各种block嵌套,抽象方法的使用。但是这库是非常优秀的,他教会了我们如果提供一个简单的接口方案。教会了我们如何将复杂的东西让人用起来如此简单;

调用链设计

Masony 使用了大量的block实现了链式调用,链式的使用节省了大段代码边写,且更容易理解。

抽象方法的实现

使用宏实现抽象方法:

1
2
3
4
#define MASMethodNotImplemented() \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]

包装宏的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))

#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))

@interface MASConstraint (AutoboxingSupport)

- (MASConstraint * (^)(id attr))mas_equalTo;
- (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;

- (MASConstraint * (^)(id offset))mas_offset;

@end

避免循环引用的方式

使用block有两个比较烦的地方

  1. 循环引用
  2. 回调地狱

Masonry 的完美设计, 让我们完全不用关心有这样的问题。

总之,这个库足够优秀;