iOS: 谈谈 frame 和 bounds

起因

大家知道, 我们可以设置 view 的四个角或者其中一个或者几个为圆角.

使用的方法:

1
2
3
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect
byRoundingCorners:(UIRectCorner)corners
cornerRadii:(CGSize)cornerRadii;

拖好界面元素之后, 在代码中来修改其为圆角, 居然失败了.

想要的效果是这样的:
1

但是最终是这样的:
1

于是总结了一下, 分享给大家.

设置圆角

这里分两种情况.

第一种: 只放置控件, 不设置约束.

1.storyboard 中拖好控件.

注意: 这里我并没有设置任何约束.

2.vc 代码

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
@interface ViewController ()
@property (strong, nonatomic) IBOutlet UILabel *lb;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self changeLbCorner];
}
- (void)changeLbCorner
{
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.lb.frame
byRoundingCorners:UIRectCornerTopRight|UIRectCornerBottomRight
cornerRadii:CGSizeMake(7, 7)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = _lb.bounds;
maskLayer.path = maskPath.CGPath;
self.lb.layer.mask = maskLayer;
}
@end

代码编译运行到模拟器(iphone6), 看不到任何东西.

log 日志显示 lb 的信息如下:

1
2
3
[ViewController viewDidLoad]:
lb.bounds: {{0, 0}, {136, 39}}
lb.frame: {{39, 89}, {136, 39}}

位置信息是正确的, 咨询检查发现是参数传入错误, 修改 changeLbCorner 方法:

1
2
3
4
5
6
7
8
9
10
- (void)changeLbCorner
{
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.lb.bounds
byRoundingCorners:UIRectCornerTopRight|UIRectCornerBottomRight
cornerRadii:CGSizeMake(7, 7)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = _lb.bounds;
maskLayer.path = maskPath.CGPath;
self.lb.layer.mask = maskLayer;
}

这里只是将 self.lb.frame 改成了 self.lb.bounds.

再次运行可以看到效果:
1

第二种: 放置控件并设置约束.

1.设置 lb 距离父 view 左边和上边的约束.
2.运行上面的代码, 发现, lb 并没有被设置为圆角.
并且 lb 的宽度和高度变小了, 变成了文字的实际的宽高.
1

看 log:

1
2
3
[ViewController viewDidLoad]:
lb.bounds: {{0, 0}, {136, 39}}
lb.frame: {{39, 89}, {136, 39}}

其实这个时候, (从上面图中可以看出) 这里的信息是错误的.

正确的信息应该是这样的(在 viewDidAppear 中)打印信息:

1
2
3
[ViewController viewDidAppear]:
lb.bounds: {{0, 0}, {58.5, 19.5}}
lb.frame: {{39, 89}, {58.5, 19.5}}

于是, 将

1
[self changeLbCorner];

放到 viewDidAppear 中, 圆角就正常了.

接下来, 我们把 lb 的宽高(136*39)约束也加上.

看一下, viewDidLoad 和 viewDidAppear 方法中打印的信息:

1
2
3
4
5
6
[ViewController viewDidLoad]
lb.bounds: {{0, 0}, {1000, 1000}}
lb.frame: {{0, 0}, {1000, 1000}}
[ViewController viewDidAppear:]
lb.bounds: {{0, 0}, {136, 39}}
lb.frame: {{39, 89}, {136, 39}}

可以看出, viewDidLoad 中错的一塌糊涂.

这里也说明一个问题:

xib 或者 storyboard 中设置过约束(现实开发中, 基本都会设置约束)的组件, 在 viewDidLoad 中并没有完全 layout, 只是预加载了这些组件.
想获取组件如 frame 何 bounds 信息, 在 viewDidLoad 中是不合适甚至是错误的.

那么, 问题来了, 哪里合适哪里正确.

上面如果你认真看了, 在 viewDidAppear 中是可以正确获取的, 那么还有没有其他方法可以获取呢?

VC 生命周期函数

要回答上面的问题, 大家要知道 vc 的生命周期函数.

上面的例子, 可以看出: 当函数 ViewDidLoad 被调用的时候,IBQutlets 已经被连接,但View 还没有被加载出来,无法获取 frame 等信息.
可以在 viewDidLoad 中完成在 IB 中不能完成的 view 的自定义。

关于 loadView 和 viewDidLoad 在后面博客跟大家分享.

今天要说的是

1
viewDidLayoutSubviews

viewDidLayoutSubviews 在 VC 子视图位置或者尺寸 (position|size) 被改变的时候被调用.

直到 AutoLayout 已经完成工作的时候才会被确定,所以在执行完 AutoLayout 之后会调用此方法. 换句话说, view 的 frame 和 bounds 这个时候是正确可以获取的.

viewDidLayoutSubviews 这个方法在 viewDidAppear 之前被调用, 有可能会被调用多次.

即依赖 bounds 或者 frame 的操作,都应该放在viewDidLayoutSubviews 中,而不是 viewDidLoadviewWillAppear 中.

改变后的代码如下:

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
#import "ViewController.h"
#define MZLog(fmt, ...) NSLog((@"%s\n" fmt), __FUNCTION__, ##__VA_ARGS__)
#define MZLogLbInfo \
MZLog(@"lb.bounds: %@ \nlb.frame: %@", NSStringFromCGRect(self.lb.bounds), NSStringFromCGRect(self.lb.frame))
@interface ViewController ()
@property (strong, nonatomic) IBOutlet UILabel *lb;
@property (strong, nonatomic) IBOutlet UILabel *displayedText;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
MZLogLbInfo;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
MZLogLbInfo;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
MZLogLbInfo;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
MZLogLbInfo;
[self changeLbCorner];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
MZLogLbInfo;
}
#pragma mark Callback.
- (void)changeLbCorner
{
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.lb.bounds
byRoundingCorners:UIRectCornerTopRight|UIRectCornerBottomRight
cornerRadii:CGSizeMake(7, 7)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = _lb.bounds;
maskLayer.path = maskPath.CGPath;
self.lb.layer.mask = maskLayer;
}
@end

打印的 log 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[ViewController viewDidLoad]
lb.bounds: {{0, 0}, {1000, 1000}}
lb.frame: {{0, 0}, {1000, 1000}}
[ViewController viewWillAppear:]
lb.bounds: {{0, 0}, {1000, 1000}}
lb.frame: {{0, 0}, {1000, 1000}}
[ViewController viewWillLayoutSubviews]
lb.bounds: {{0, 0}, {1000, 1000}}
lb.frame: {{0, 0}, {1000, 1000}}
[ViewController viewDidLayoutSubviews]
lb.bounds: {{0, 0}, {136, 39}}
lb.frame: {{39, 89}, {136, 39}}
[ViewController viewWillLayoutSubviews]
lb.bounds: {{0, 0}, {136, 39}}
lb.frame: {{39, 89}, {136, 39}}
[ViewController viewDidLayoutSubviews]
lb.bounds: {{0, 0}, {136, 39}}
lb.frame: {{39, 89}, {136, 39}}
[ViewController viewDidAppear:]
lb.bounds: {{0, 0}, {136, 39}}
lb.frame: {{39, 89}, {136, 39}}

frame 和 bounds

上面的例子, 大家看到由于传入了 frame 而不是 bounds 造成设置圆角失败.

下面说说 frame 和 bounds.

概念

从网上”偷”过来的图

1

1.frame

该 view 在父 view 坐标系统中的位置和大小(参照点是,父坐标系统).

2.bounds

该 view 在本地坐标系统中的位置和大小(参照点是,本地坐标系统,就相当于 view 自己的坐标系统,以(0,0)点为起点).
其实本地坐标系统的关键就是要知道的它的原点(0,0).

bounds 默认值是(0, 0, width, height).除非手动改变 bounds 的值.

单纯的从概念上面, 很难理解二者的区别.

提供一个例子, 例子大概是这样的:
redView 是 yellowView 的父视图, yellowView 是 blueView 的父视图.

通过改变 redView 的 bounds 会影响子视图的位置(不是frame).
将 redView 的 bounds 起点设为(-20, -20), 子视图相对于 redView 的本地坐标(0, 0), 也就需要往下增加20, 这样, yellowView 就往下移动了.

效果图:

1

1

1

完整代码

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#import "ViewController.h"
#define MZLog(fmt, ...) NSLog((@"%s\n" fmt), __FUNCTION__, ##__VA_ARGS__)
#define MZLogLbInfo \
MZLog(@"lb.bounds: %@ \nlb.frame: %@", NSStringFromCGRect(self.lb.bounds), NSStringFromCGRect(self.lb.frame))
@interface ViewController ()
@property (strong, nonatomic) IBOutlet UILabel *lb;
@property (strong, nonatomic) IBOutlet UILabel *displayedText;
@end
@implementation ViewController
{
UIView *redView;
UIView *yellowView;
UIView *blueView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
MZLogLbInfo;
// 将 redView 添加到 self.view
{
redView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 120, 120)];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
}
// 将 yellowView 添加到 redView
{
yellowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 90, 90)];
yellowView.backgroundColor = [UIColor yellowColor];
[redView addSubview:yellowView];
}
// 将 blueView 添加到 yellowView
{
blueView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 60, 60)];
blueView.backgroundColor = [UIColor blueColor];
[yellowView addSubview:blueView];
}
[self logViewInfo];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
MZLogLbInfo;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
MZLogLbInfo;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
MZLogLbInfo;
[self changeLbCorner];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
MZLogLbInfo;
}
#pragma mark Callback.
- (void)changeLbCorner
{
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.lb.bounds
byRoundingCorners:UIRectCornerTopRight|UIRectCornerBottomRight
cornerRadii:CGSizeMake(7, 7)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = _lb.bounds;
maskLayer.path = maskPath.CGPath;
self.lb.layer.mask = maskLayer;
}
- (IBAction)doResetAction:(id)sender
{
[UIView animateWithDuration:1.0f animations:^{
[redView setBounds:CGRectMake(0, 0, 120, 120)];
[yellowView setBounds:CGRectMake(0, 0, 90, 90)];
} completion:^(BOOL finished) {
[self logViewInfo];
}];
}
- (IBAction)doChangeRedViewBounds:(id)sender
{
[UIView animateWithDuration:1.0f animations:^{
[redView setBounds:CGRectMake(-20, -20, 120, 120)];
} completion:^(BOOL finished) {
[self logViewInfo];
}];
}
- (IBAction)doChangeYellowViewBounds:(id)sender
{
[UIView animateWithDuration:1.0f animations:^{
[yellowView setBounds:CGRectMake(-20, -20, 90, 90)];
} completion:^(BOOL finished) {
[self logViewInfo];
}];
}
#pragma mark Display Debug Info.
- (void)logViewInfo
{
NSString *log4rView = [NSString stringWithFormat:@"RedView\nframe:%@ \nbounds:%@",
NSStringFromCGRect(redView.frame), NSStringFromCGRect(redView.bounds)];
NSString *log4yView = [NSString stringWithFormat:@"YellowView\nframe:%@ \nbounds:%@",
NSStringFromCGRect(yellowView.frame), NSStringFromCGRect(yellowView.bounds)];
NSString *log4bView = [NSString stringWithFormat:@"BlueView\nframe:%@ \nbounds:%@",
NSStringFromCGRect(blueView.frame), NSStringFromCGRect(blueView.bounds)];
NSString *log = [NSString stringWithFormat:@"%@\n%@\n%@", log4rView, log4yView, log4bView];
[self display:log];
}
- (void)display:(NSString *)content
{
self.displayedText.text = content;
}
@end

总结

  • frame, 描述的是当前视图在其父视图中的位置和大小.
    bounds, 描述的是当前视图在其自身坐标系统中的位置和大小.

所以, bounds 默认是 (0, 0, frame.size.width, frame.size.height)

另外, 还有一个 center 描述的是当前视图的中心点在其父视图中的位置.

  • bounds 和 frame 是两个不等同的概念, 改变 bounds 会影响子视图的位置(人眼看到其改变了位置), 设置 bounds 可以修改自己坐标系的原点位置. 但是不会改变子视图的 bounds 和 frame.

明白上面的道理很重要, iOS 中滚动视图能让你看到其中的内容, 正是利用了 contentoffset 和 bounds 属性.

这里以 tableView 为例子, 当我们向上滚动 tableView, tableView 的 contentOffset 和 bounds 的坐标都是正数, 相当于其本地坐标(0, 0)改变了即增加了(坐标系往下为增加), 那么其子视图就会向上去.

向下滑动时, 也是同一个道理.
可以通过运行 完整 Demo 中[查看 TableView]按钮来打开例子, 看日志.

  • 改变子视图所有父视图的 bounds, 子视图的位置是累加改变的.
    如上面改变 redView 和 yellowView 的 bounds, blueView 的位置相对 redView 往下移动了 40.

  • 当同一个视图的 bounds 大于 frame, 会导致 frame 被撑大,frame 的 x, y, width, height 都会被改变. 反之, bounds 小于 frame, frame 也会变小.

附录

  1. GitHub 上面可以下载 完整 Demo

  2. 推荐之前写在 CSDN 上的博文: iOS UI 技巧: 视图无法被点击

可关注我的微信公众号:
1

坚持原创技术分享!