前言
听说首图能吸引人点进来用Masonry
时,刚设置完布局后想使用frame
干点坏事,发现并不是期望的值。
- (void)viewDidLoad {
self.btn = [[UIButton alloc] init];
[self.view addSubview:self.btn];
[self.btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.width.equalTo(@100);
make.height.equalTo(@50);
}];
NSLog(@"%@",self.btn);// 输出frame = (0 0; 0 0)
// [self.view layoutIfNeeded];
}
然后试了在viewWillAppear
首次出现界面还是没获取到期望值,
在viewDidAppear
才获取到期望值。
解决方法:在viewDidLoad
定义完Masonry
的bolck
后调用一下[self.view layoutIfNeeded]
,就能马上获取到期望值。
猜测应该是VC
调用了某些布局方法。
并且这些方法在viewWillAppear
和DidAppear
之间也调用了。
现在就来分析为什么会这样。
controller对view的布局时机和所用方法
VC的生命周期
这个执行顺序挺容易理解的。
alloc:创建对象,分配空间
init (xib和非xib用initWithNibName、stroyBoard用initWithCoder) :初始化对象,初始化数据
awakeFromNib:(若控制器有关联xib才调用这方法)
loadView:优先从nib载入控制器视图 ,其次代码
viewDidLoad:载入完成,可以进行自定义数据以及动态创建其他控件。
viewWillAppear:视图将出现在屏幕之前,马上这个视图就会被展现在屏幕上了
viewWillLayoutSubviews:控制器的view将要布局子控件
viewDidLayoutSubviews:控制器的view布局子控件完成
//这期间系统可能会多次调用viewWillLayoutSubviews 、 viewDidLayoutSubviews 俩个方法
viewDidAppear:视图已在屏幕上渲染完成
viewWillDisappear:视图将被从屏幕上移除之前执行
viewDidDisappear:视图已经被从屏幕上移除,用户看不到这个视图了
dealloc:视图被销毁,此处需要对你在init和viewDidLoad中创建的对象进行释放
didReceiveMemoryWarning:收到内存警告
调用顺序
可以看到和布局有关的两个方法确实夹在viewWillAppear
和viewDidAppear
之间,且有可能会调用多次,但只有在首次appear
才会自动调用。
(实测的时候发现若在viewDidLoad
中,self.view加了(无论多少个)子控件VC会分别调用两次这些方法,没添加子控件分别调用一次)
接下来分析[self.view layoutIfNeeded]为什么能实现立即刷新frame
首先,Masonry
是建立在autolayout
之上的,最终转化为frame
。
一开始让我惊讶的是,Masonry
约束的bolck
会马上执行。但frame
不能立即获取。只能说该约束要在别的方法才能转frame
。
结论:
viewDidLoad
定义完Masonry
的block
后,(从上图可以看出过了少于0.1秒的时间内)两布局方法就调用完成,frame
也被算出来并在画面上描绘好view
了。
如果定义完后直接调用[self.view layoutIfNeeded]
后,VC会在该函数内马上同步调用viewWillLayoutSubviews
和viewDidLayoutSubviews
各一次,这时候frame
就是期望值了。
以上弄清楚Masonry
不能立即获取frame
的原因了。但都是分析VC
对UIView
的布局方式。那么view
中实现内部布局又是怎么个程序执行顺序呢?
UIView内部布局执行顺序
布局相关方法
- 可以分为三块
updateConstraints
<-->layout
-->display
前两个与布局有关,第三个与渲染有关。
// 三块的主要方法
#pragma mark - updateConstraints
//看上去好像set和get方法,但是set方法并无参数,调用就会标记为YES。
//init后调用get方法发现是YES。
setNeedsUpdateConstraints:标记需要updateConstraints。
needsUpdateConstraints:返回是否需要updateConstraints。
updateConstraintsIfNeeded:若需要,马上updateConstraints。
updateConstraints:更新约束,自定义view应该重写此方法在其中建立constraints. 注意:要在最后调用[super updateConstraints]
#pragma mark - layout
layoutIfNeeded:使用此方法强制立即进行layout,从当前view开始,此方法会遍历整个view层次(包括superviews)请求layout。因此,调用此方法会强制整个view层次布局。
setNeedsLayout:此方法会将view当前的layout设置为无效的,并在下一个upadte cycle里去触发layout更新。
layoutSubviews:如果你需要更精确控制子view,而不是使用限制或autoresizing行为,就需要实现该方法。
#pragma mark - display
setNeedsDisplay:标记整个视图的边界矩形需要重绘.
drawRect:如果你的View画自定义的内容,就要实现该方法,否则避免覆盖该方法。
分享一下我对这些方法的理解,应该对理解后面过程有帮助。如果有错误的地方,欢迎指出来。
- 整体分成了 怎么执行 、 需要执行的标记 和 马上执行布局。
-
怎么执行的三类方法
layoutSubviews
、draw
和updateConstraints
只应该被重载,绝不要在代码中显式地调用,系统会在需要的时候自动调用。
举个例子
某个view.m
- (void)updateConstraints {
[self.sourceCollectionView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
//super必须写在最后
[super updateConstraints];
}
- (void)setModelArray:(NSArray *)modelArray{
CGRect newFrame = self.frame;
newFrame.size.height = modelArray.count * 35;
self.frame = newFrame;
[self updateConstraintsIfNeeded];
}
updateConstraints
里写好了view
内部某个Collectionview
的布局。当传入模型后,view
的高度改变,调用updateConstraintsIfNeeded
或者setNeedsUpdateConstraints
,而不要显示调用updateConstraints
,在VC调用布局方法时自然会跑这个方法。
- 很多情况下系统都会把view的需要执行标记置为YES。
-
updateConstraints
是子控件对父控件的。
layoutSubviews
是父控件对子控件的。会递归调用子控件的layoutSubviews
。
display
先渲染父控件,再渲染子控件。 - 布局运行在
update cycle
中,一般不卡的话,1/60s就会更新一遍。 -
view
的以上三个执行标记发生改变,要等到下一次update cycle
后,VC才会调用布局方法计算好frame并渲染到屏幕上。 -
updateConstraintsIfNeeded
、layoutIfNeeded
这两个马上执行方法是给我们调用的,告诉系统不用等到下一个update cycle
,VC马上执行布局方法。 -
view
的init
后调用needsUpdateConstraints
返回YES
。而暴露的set方法只能标记为YES
,作用应该是告诉系统下一次cycle
要更新约束。猜测底层布局好后会有别的set方法置为NO。
分析执行顺序
- 情形1
创建HSUTestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.testView = [[HSUTestView alloc] init];
self.testView.backgroundColor = [UIColor blueColor];
[self.view addSubview:self.testView];
[self.testView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.width.equalTo(@100);
make.height.equalTo(@50);
}];
}
UIView内部执行顺序
可以看到init
后把三个标记都置为YES
。
然后在VC的布局方式中,viewWillLayoutSubviews
中会调用updateConstraints
,在viewDidLayoutSubviews
会调用layoutSubviews
,draw
。所以说不要显示调用 怎么执行 这三个方法。
- 情形2
创建HSUContentView 然后add HSUTestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
HSUContentView *contentView = [[HSUContentView alloc] init];
[self.view addSubview:contentView];
[contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.width.equalTo(@100);
make.height.equalTo(@50);
}];
self.testView = [[HSUTestView alloc] init];
self.testView.backgroundColor = [UIColor blueColor];
[contentView addSubview:self.testView];
[self.testView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(contentView);
}];
}
view加view的执行顺序
可以看到
updateConstraints
是子到父。layoutSubviews
和drawRect
是父到子。
最后一张图总结UIView内部布局执行顺序与VC的交互
UIView内部布局执行顺序与VC的交互补充 以下情形会调用layoutSubviews
1、init初始化不会触发layoutSubviews
但是是用initWithFrame 进行初始化时,当rect的值不为CGRectZero时,也会触发
2、addSubview会触发layoutSubviews
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
4、滚动一个UIScrollView会触发layoutSubviews
5、旋转Screen会触发父UIView上的layoutSubviews事件
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件
7、直接调用setLayoutSubviews。
8、直接调用setNeedsLayout。
网友评论