目录
1 constraints & layout
iOS autolayout对于视图的布局比autoresizing要灵活很多。但是autolayout的代码里的实现并不是那么用户友好:literal语法之复杂被戏称为“象形文字”;同时autolayout API非常冗长。Masonry框架是对autolayout相关类的封装,使得在code里执行autolayout变得紧凑和更加简洁。在使用过几次Masonry后,笔者碰到一些诸如在哪里写Masonry constraints,以及何时UIView和subviews的frame确定下来等问题。本文通过1个小例子对上述问题进行澄清。
1.1 constraints
NSConstraint
是autolayout的基石和前提。开发者写好NSConstraint
,OC Runtime根据NSConstraint
来layout视图。这是autolayout名字的由来,即通过更高一级的抽象,在NSConstraint
层面写好约束关系,由运行时进行布局。
那么constraints在哪里写比较好呢。我们可以将UIView分为3个等级:
- view controller’s view,下面称为
L0View
; - view controller’s view’s children subviews,下面称为
L1View
; - view controller’s view’s grandson subviews,下面称为
L2View
;
Constraints Hierarchy:一个普遍的原则是,某view自身在其superView里的constraints必须由其superView来实现;而某view的children subviews在该view里的constraints必须由该view来实现。换句话说,你的位置由你的上级决定,但是你可以决定你的直接下级的位置。这样做的好处是constraints的解耦,利于代码复用。如果某view自身在其superView里的constraints必须由该view自身实现,那么换了superView之后,又得重新更新constraints。
在上面的3个等级中,对于L1View
,其constraint由L0View
决定;而L2View
的constraint由L1View
决定。通常情况下,L0View
作为view controller的view
属性,我们一般很少customize;如果需要customize,也可以从L1View
开始,将L0View
作为L1View
的容器。因此对于L1View
的constraint,我们可以在直接和L0View
紧密相连的view controller里实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.lcView = [[LCView alloc] initWithFrame:CGRectMake(10, 10, 300, 100)];
self.lcView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.lcView];
}
-(void)updateViewConstraints{
//Step 1
NSLog(@"begin %@,%@",self,NSStringFromSelector(_cmd));
[super updateViewConstraints];
[self.l1View mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.l1View.superview.mas_centerY).with.multipliedBy(1.0);
make.left.equalTo(self.l1View.superview.mas_left).with.offset(5.0);
make.right.equalTo(self.l1View.superview.mas_right).with.offset(-5.0);
make.height.equalTo(self.l1View.mas_width).with.multipliedBy(0.3);
}];
NSLog(@"finish %@,%@",self,NSStringFromSelector(_cmd));
}
-(void)viewWillLayoutSubviews{
//Step 2.1, L1View的frame还没确定
NSLog(@"%@,%@",self,NSStringFromSelector(_cmd));
}
-(void)viewDidLayoutSubviews{
//Step 2.2, L1View的frame已经确定, 可以在这里进行frame的微小人工调整
NSLog(@"%@,%@",self,NSStringFromSelector(_cmd));
}
@end
updateViewConstraints
的方法其实更好的命名(当然不够规范)应该是updateL1ViewsConstraints
,因为根据Constraints Hierarchy,view controller自身的view的constraints应该由其上一级决定,这里可以是push时的presenting view controller来决定presented view controller的view的constraints。
在添加L1View
的constraint后,对于L2View
的constraint是在L2View
的updateConstraints
里添加的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@implementation L1View
//from storyboard
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
NSLog(@"%@,%@",self,NSStringFromSelector(_cmd));
if(self = [super initWithCoder:aDecoder]){
[self setup];
}
return self;
}
-(void)awakeFromNib{
NSLog(@"%@,%@",self,NSStringFromSelector(_cmd));
[super awakeFromNib];
}
//from code
-(instancetype)initWithFrame:(CGRect)frame{
NSLog(@"%@,%@",self,NSStringFromSelector(_cmd));
if(self = [super initWithFrame:frame]){
[self setup];
}
return self;
}
-(void)setup{
self.backgroundColor = [UIColor greenColor];
_iconView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
_iconView.backgroundColor = [UIColor blueColor];
_titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, 50, 50, 50)];
_titleLabel.text = @"Title";
_titleLabel.backgroundColor = [UIColor grayColor];
_detailLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, 100, 50, 50)];
_detailLabel.text = @"detailTitle";
_detailLabel.backgroundColor = [UIColor grayColor];
[self addSubview:_iconView];
[self addSubview:_titleLabel];
[self addSubview:_detailLabel];
}
-(void)updateConstraints{
//Step 1V add L2View约束
NSLog(@"%@,%@",self,NSStringFromSelector(_cmd));
[super updateConstraints];
/* You can update Constraints for self, but it is better to only update constraints for subcell and move the logic to update constraints for self to its superview (controller)
[self mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.superview.mas_centerY).with.multipliedBy(1.0);
make.left.equalTo(self.superview.mas_left).with.offset(5.0);
make.right.equalTo(self.superview.mas_right).with.offset(-5.0);
make.height.equalTo(self.mas_width).with.multipliedBy(0.3);
}];
*/
[self.iconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.iconView.superview.mas_centerY).with.multipliedBy(1.0);
make.left.equalTo(self.iconView.superview.mas_left).with.offset(5.0);
make.height.equalTo(self.iconView.superview.mas_height).with.multipliedBy(0.7);
make.width.equalTo(self.iconView.mas_height).with.multipliedBy(1.5);
}];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.iconView.mas_top).with.offset(0);
make.left.equalTo(self.iconView.mas_right).with.offset(10);
make.right.equalTo(self.titleLabel.superview.mas_right).with.offset(-10);
make.height.equalTo(self.iconView.superview.mas_height).with.multipliedBy(0.15);
}];
[self.detailLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.titleLabel.mas_bottom).with.offset(5);
make.bottom.equalTo(self.iconView.mas_bottom).with.offset(0);
make.left.equalTo(self.iconView.mas_right).with.offset(10);
make.right.equalTo(self.detailLabel.superview.mas_right).with.offset(-10);
}];
}
-(void)layoutSubviews{
//Step 2V
NSLog(@"%@,%@",self,NSStringFromSelector(_cmd));
//L2View Frame 还没确定
[super layoutSubviews];
//L2View Frame 已经确定,可以在下面微小调整L2View frame
[self setNeedsUpdateConstraints];
CGRect frame = self.iconView.frame;
frame.size.width = 50;
self.iconView.frame = frame;
}
@end
值得一提的是,如果你在storyboard设置了layout某个选项,例如l1View
的centerY
,那么如果你在updateViewConstraints
里也设置了和centerY
相关的constraint,就会出现conflict。The best practice is to purely use masonry
and not use storyboard autolayout at all。这和其update
的名字有些冲突,用add
应该更合适。
1.2 layout
在UIViewController
的updateViewConstraints
和L1View
的updateConstraints
规定约束条件后,接下来就是实施这些条件来确定相关view的frame。前者包含了viewWillLayoutSubviews
(L1View
约束还没实施,L1View
的frame
还没确定)和viewDidLayoutSubviews
(L1View
约束已实施,L1View
的frame
已确定)。后者只有layoutSubviews
(L2View
约束还没实施,L1View
的frame
还没确定,需要call
[super layoutSubviews],
L2View约束已实施,
L1View的
frame``已确定)。
2 总结
最后将Constraint总结成下图。