iPhone 设备的屏幕信息
设备 | 屏幕尺寸(英寸) | 像素(px) | pixels pre point | 图片适配方案 |
---|---|---|---|---|
iPhone4,4s | 3.5 | 640*960 | 2 | @2x |
iPhone 5,5s,SE | 4.0 | 640*1136 | 2 | @2x |
iPhone 6,6s,7 | 4.7 | 750*1334 | 2 | @2x |
iPhone 6 plus, 6s plus, 7 plus | 5.5 | 1080*1920 | 3 | @3x |
视图的层次结构
任何一个应用都有且只有一个 UIWindow 对象,负责包含应用中的所有视图
- 层次结构中的每个视图(包括 UIWindow 对象)分别绘制自己。视图会将自己绘制到图层( layer )中,每个 UIView 对象都有一个 layer 属性,指向一个 CALayer 对象
- 所有视图的图层组合成一幅图像,绘制到屏幕
- UI 界面展示流程:UIApplication — UIWindow — UIViewController — UIView — …
CGRect / CGSize / CGPoint
CGRectMake()
CGRectGetMaxX()
CGRectGetMaxY()
CGRectGetMinX()
CGRectGetMinY()
CGRectGetWidth()
CGRectGetHeight()
CGRectContainsPoint(,)
// 扩大或缩小
CGRectInset(,,)
frame / bounds / center
frame 、 bounds 和 center 是视图的三个属性,用于确定 view 在屏幕中的位置
- frame 保存的是距离当前视图左上角的大小( size )和相对于父视图中距离原点的位置( origin ),所表示的矩形位于父视图的坐标系,用于确定与视图层次结构中其他视图的相对位置
- bounds 表示的矩形位于自己的坐标系,origin 指当前视图距离自身坐标系左上角的位置,size 当前视图在自身坐标系中的高宽,用于确定绘制区域
- center 是视图矩形的中心点坐标,表示当前 view 在父 view 中的位置
设有两 view ,view2 为 view1 的 subView
- 修改 view1 的 frame.origin.x
frame | bounds | center | |
---|---|---|---|
view1 | x 改变 | 不变 | x 改变 |
view2 | 不变 | 不变 | 不变 |
- 修改 view2 的 frame.origin.x
frame | bounds | center | |
---|---|---|---|
view1 | 不变 | 不变 | 不变 |
view2 | x 改变 | 不变 | x 改变 |
修改父 view 的 frame.origin 的值,移动了父 view 的位置,子 view 在父 view 中的位置没有修改,只影响当前 view 的位置
- 修改 view1 的 bounds.origin.x
frame | bounds | center | |
---|---|---|---|
view1 | 不变 | x 改变 | 不变 |
view2 | 不变 | 不变 | 不变 |
view2显示的视图位置改变
修改父 view 的 bounds.origin 的值,父 view 的位置没有修改,但是影响了子 view 在父 view 的位置,但是相应的位置信息没有修改
- 修改 view2 的 bounds.origin.x
frame | bounds | center | |
---|---|---|---|
view1 | 不变 | 不变 | 不变 |
view2 | 不变 | x 改变 | 不变 |
修改子 view 的 bounds.origin 的值,没有影响父 view 的位置
- 修改 view1 的 frame.size.width
frame | bounds | center | |
---|---|---|---|
view1 | width 改变 | width 改变 | 改变 |
view2 | 不变 | 不变 | 不变 |
- 修改 view2 的 frame.size.width
frame | bounds | center | |
---|---|---|---|
view1 | 不变 | 不变 | 不变 |
view2 | width 改变 | width 改变 | 改变 |
修改父 view 的 frame.size 的值,只影响当前 view 相对于左上角的高宽大小
- 修改 view1 的 bounds.size.width
frame | bounds | center | |
---|---|---|---|
view1 | x,width 都改变 | width 改变 | 不变 |
view2 | 不变 | 不变 | 不变 |
修改父 view 的 bounds.size 的值,父 view 的位置进行了移动,影响了子 view 在父 view 的位置,但是相应的位置信息没有修改
- 修改 view2 的 bounds.size.width
frame | bounds | center | |
---|---|---|---|
view1 | 不变 | 不变 | 不变 |
view2 | x,width 都改变 | width 改变 | 不变 |
修改子 view 的 bounds.size 没有影响父 view 的位置信息,但是影响了子 view 的位置信息
三种布局方式
- frame 绝对定位布局
- frame + autoResizing 相对布局
.autoresizesSubviews
.autoresizingMask - AutoLayout 相对布局
通过添加约束对象,可以设置任何控件之间的关系
界面的实现
- 纯代码
- Interface Builder
通过 Interface Builder 绘制的文件格式为 xib 文件,在编译过程将 xib 文件转化为二进制的 nib 文件
一般一个 xib 文件对应一个 viewController ,同时也可以用来绘制可重用的 UIView 视图
xib 通过 UINib 类进行读取
加载 xib 文件有两种方式:
// xib 文件在 iOS 中做为资源文件存在,因此可以使用 NSBundle 的方式进行加载
[[NSBundle mainBundle] loadNibNamed: owner: options:];
// 在iOS4.0以后新增了[UINib class] 用于加载nib资源文件
[UINib nibWithNibName: bundle:];
- Storyboard
Storyboard 通过 UIStoryboard 类进行读取
AutoLayout
布局属性
Width / Height
Top / Bottom / Left / Right
CenterX / CenterY
BaseLine
Leading / Trailing
约束
NSLayoutAttributeWidth / NSLayoutAttributeHeight
NSLayoutAttributeTop / NSLayoutAttributeBottom / NSLayoutAttributeLeft / NSLayoutAttributeRight
NSLayoutAttributeTopMargin / NSLayoutAttributeBottomMargin / NSLayoutAttributeLeftMargin / NSLayoutAttributeRightMargin
NSLayoutAttributeCenterX / NSLayoutAttributeCenterY
NSLayoutAttributeBaseline
NSLayoutAttributeLeading / NSLayoutAttributeTrailing
NSLayoutAttributeLeadingMargin / NSLayoutAttributeTrailingMargin
NSLayoutAttributeNotAnAttribute
- 通常情况下,在大部分语言体系中都是从左往右进行阅读,则 NSLayoutAttributeLeft 与 NSLayoutAttributeLeading 都表示内容左侧,NSLayoutAttributeRight 与 NSLayoutAttributeTrailing 都表示内容右侧。在部分特殊语系中从右往左进行阅读,这时 NSLayoutAttributeLeft 与 NSLayoutAttributeTrailing 都表示内容左侧,NSLayoutAttributeRight 与 NSLayoutAttributeLeading 都表示内容右侧
- Xcode 8.0 版本,视图与ViewController根视图之间 左右间距的系统推荐的间距值为 16pt。Xcode 8.0 版本及之前版本,两个普通父子视图之间 上下左右间距的系统推荐的间距值都为 8pt
- 在 Interface Builder 中,当勾选 Constrain to margins 时,如添加右侧边界约束时,实际约束对象建立的是 NSLayoutAttributeTrailingMargin 边界约束,当不勾选 Constrain to margins 时,实际约束对象建立的是 NSLayoutAttributeTrailing 边界约束
- NSLayoutAttributeNotAnAttribute 一般用于自身约束如设置视图宽度用
- 容器 view 的高度可以根据子 view 的属性进行确定
Interface Builder 下约束的设置界面
代码中新建约束
如果使用代码添加约束,则需要关闭 autoResizing mask 的布局方式以免冲突
view.translatesAutoresizingMaskIntoConstrains = NO;
我们可以将约束口语化描述为:
view1.attrubute1 = view2.attribute2 * multiplier + constant
则对应的代码中的方法,有
[NSLayoutConstraint class]
+ constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:
Virtual Format Language
功能 | 表达式 |
---|---|
水平方向 | H: |
垂直方向 | V: |
视图 | [view] |
父视图 | \ |
关系 | >=、==、<= |
间隔 | - |
优先级 | @value |
[NSLayoutConstraint class]
+ constraintsWithVisualFormat:options:metrics:views:
// format 为 NSString 对象,即视觉化格式语言,eg:
H:[button]-8-[Label]
H:|-20-[button]-20-|
H:|-[button(==30)]-|
H:[button1(==60)]-10-[button2(==button1)]
// options 为 NSLayoutFormatOptions 枚举值
// metrics 为 NSDictionary 对象,存放在 format 中出现的 String 和对应的值,key 必须为 NSString ,value 为 NSNumber
// views 为 NSDictionary 对象,存放在 format 中的 View 的名字和对应的 view ,key 必须为 NSString ,value 为 view 实例
可以使用
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(button1, button2);
即创建字典 { @"button1" = button1, @"button2 = button2 }
添加约束的对象
[UIView class]
- addConstraint:
- addConstraints:
约束添加规则:
- 约束对多个父 view 相同的 views 起作用,则添加到父 view
- 约束对 view 自身起作用,添加到 view 自身
- 约束对多个父 view 不同的 views 起作用,添加到他们最近一级的祖先 view
- 约束对 view 和父 view 起作用,添加到父 view
约束的查找与删除
[UIView class]
// NSArray 对象,保存了 view 的所有 NSLayoutConstraint
.constraints
// 删除约束
- removeConstraint:
- removeConstraints:
// NSLayoutConstraint 有 .firstItem / .firstAttribute / .relation / .secondItem / .secondAttribute / .multiplier / .constant 这几个属性,可以用来查找确定一个约束
for (NSLayoutConstraint *constraint in _contentScrollView.constraints) {
if ([constraint.firstItem isKindOfClass:[UIView class]] && constraint.firstAttribute == NSLayoutAttributeBottom && constraint.firstItem == _preView) {
[_contentScrollView removeConstraint:constraint];
}
}
优先级
约束有属性 .priority(优先级),当约束冲突时,低优先级的约束被忽略
数值从1 - 1000,分为三档:
- 1000(required) -- UILayoutPriorityRequired
- 750(high) -- UILayoutPriorityDefaultHigh
- 250(low) -- UILayoutPriorityDefaultLow
默认情况下为1000
内容优先级
- 固有内容大小( Intrinsic Content Size ):视图实际显示的实际内容大小。如 UILabel 的固有内容大小由字数和字体决定,UIImageView 的固有内容大小由图片的尺寸决定
[UIView class]
- intrinsicContentSize
- AutoLayout 会为固有内容大小添加约束,这类约束有两个优先级属性,分别是 内容放大优先级( content hugging priority ) 和 内容缩小优先级( content compression resistance priority )
[UIView class]
// axis -- UILayoutConstraintAxisVertical、UILayoutConstraintAxisHorizontal
- setContentHuggingPriority:forAxis:
- setContentCompressionResistancePriority:forAxis:
- content hugging priority: 表示视图固有内容大小的放大优先级。优先级越高,表示越不允许自动布局系统基于固有内容大小放大视图尺寸,默认值为 251
- content compression resistance priority: 表示视图固有内容大小的缩小优先级。优先级越高,表示越不允许自动布局系统基于固有内容大小缩小视图尺寸,默认值为 750
- 当约束的优先级大于等于固有内容的布局优先级时,会忽略内容布局的设置
当设置一个imageView(50* 50)的宽度约束为40(约束优先级为required),内容优先级为默认,显示大小为40。将约束优先级设置为DefaultHigh - 1,显示大小为50
当父视图的宽度不足容纳两个label固有内容,两个label hugging 和 compression 相同,设置labelA的宽度约束为100(优先级为DefaultHigh - 1),宽度约束仍然会被满足
Size class
主要用来适应设备屏幕大小的变化,不需要再根据屏幕大小的尺寸进行区分,而只需要对矩形区域的选择,系统会自动适配各个屏幕
//UIInterface.h
enum UIUserInterfaceSizeClass {
UIUserInterfaceSizeClassUnspecified //高或宽占据两个区域
UIUserInterfaceSizeClassCompact //高或宽占据一个区域
UIUserInterfaceSizeClassRegular //高或宽占据三个区域
}
苹果各设备高宽对应的枚举值
iPhone 4s/5/SE/6/6s/7 | iPhone 6 plus/6s plus/7 plus | iPad | |
---|---|---|---|
竖屏 | (w:Compact h:Regular) | (w:Compact h:Regular) | (w:Regular h:Regular) |
横屏 | (w:Compact h:Compact) | (w:Regular h:Compact) | (w:Regular h:Regular) |
在 Interface Builder 中我们可以根据设备对 Size Class 进行选择
当我们点击某一视图,在 Attributes inspector 下最底部分别有两项(installed (wAny hAny)和 wC hR installed (如果没有可添加)),可根据需要进行选中,选中表示在当这个 size class 下会显示这个视图
同样对于约束,在 Attributes inspector 下最底部也可选择对应约束在哪种 size class 下生效
代码中的实现
// 这个类主要封装了水平和垂直方向的 size class 等信息
// UIKit 中大多数 UI 的基础类 UIView UIViewController UIWindow ... 都实现了 <UITraitEnvironment> 协议,其中的 .traitCollection 属性为 UITraitCollection 对象,从而可以得到当前视图以及视图的约束布局的 size class 属性
[UITraitCollection class]
Top Layout Guide / Bottom Layout Guide
通常绘制 UIViewController 的内容会有几种状态:
- 显示在顶部 status bar( 20pt )下方
- 显示在navigation bar( 44pt )下方
- 显示在 tab bar( 44pt )上方
而我们绘制 UIViewController 并不知道其他 bar 的内容或是否存在
控制 UIViewController 的内容在可见区域内进行显示,[UIViewController class] 有两个属性:.topLayoutGuide 和 .bottomLayoutGuide ,设置了 VC 顶边与 topLayoutGuide 关联,底边与 bottomLayoutGuide 关联。也就是当 navigationBar 显示时,topLayoutGuide 表示 navigationBar 的底边……
则我们在设置约束时,可设置 view 与 topLayoutGuide 或 bottomLayoutGuide 的相对布局关系
布局流程
UIView 布局流程和布局更新流程
- 首先初始化控件
- 添加到父视图
- “设置控件的位置信息” —— 通过约束布局时通常不会设置frame信息
- 计算约束布局
- 更新布局
则步骤和对应方法为
步骤 | 方法 |
---|---|
Load Views | - (instancetype)init |
Update Views’ Constrains | - (void)updateConstraints |
Update Views’ Frame | - (void)layoutSubviews |
Display Views |
如果更新了布局,则重复 Update Views’ Constrains、Update Views’ Frame 和 Display Views 步骤
UIViewController 布局流程
步骤 | 方法 |
---|---|
Load Views | - (void)awakeFromNib - (void)loadView - (void)viewDidLoad |
- (void)viewWillAppear | |
Update Views’ Constrains | - (void)updateViewConstraints |
Update Views’ Frame | - (void)viewWillLayoutSubviews - (void)viewDidLayoutSubviews |
Display Views | |
- (void)viewDidAppear |
UIViewController 除了维护一个视图层外,还会处理视图变化的相关通知,即 viewWillAppear、viewDidAppear、viewWillDisappear、viewDidDisappear
viewWillAppear 主要用来设置一些简单的显示动画,或者是状态栏风格的修改
布局流程方法的执行顺序
- 打开一个 ViewController
viewDidLoad
viewWillAppear
// 根视图大小与当前屏幕大小相同,因此 updateViewConstraints 只调用一次
updateViewConstraints
viewWillLayoutSubviews
viewDidLayoutSubviews
viewDidAppear
或
viewDidLoad
viewWillAppear
updateViewConstraints
viewWillLayoutSubviews
viewDidLayoutSubviews
viewWillLayoutSubviews
viewDidLayoutSubviews
viewDidAppear
//当子 View 有 UIButton 类的视图时出现
- 打开另一个 ViewController
viewWillDisappear
updateViewConstraints
viewWillLayoutSubviews
viewDidLayoutSubviews
viewDidDisappear
- 返回 ViewController
viewWillAppear
updateViewConstraints
viewWillLayoutSubviews
viewDidLayoutSubviews
viewDidAppear
updateViewConstraints
viewWillLayoutSubviews
viewDidLayoutSubviews
- ViewController 子视图修改布局
viewWillLayoutSubviews
viewDidLayoutSubviews
- 添加一个没有子 View 的 View 到 ViewController 的 view 作为子 view
ViewController viewWillLayoutSubviews
View updateConstraints
ViewController viewDidLayoutSubviews
View layoutSubviews
- 修改子 View 使子 View 包含一个子 View,即添加一个有子 View 的子 View 到 ViewController 的 view
ViewController viewWillLayoutSubviews
View updateConstraints
ViewController viewDidLayoutSubviews
View layoutSubviews
或
ViewController viewWillLayoutSubviews
View updateConstraints
ViewController viewDidLayoutSubviews
View layoutSubviews
ViewController viewWillLayoutSubviews
ViewController viewDidLayoutSubviews
- 修改子 View 的子 View 的布局
View layoutSubviews
- 使子 View 的布局为约束布局,同时使子 View 的位置发生变化,大小不变
ViewController viewWillLayoutSubviews
ViewController viewDidLayoutSubviews
View layoutSubviews
总结
- UIViewController 首次加载时,在 viewWillAppear 之后调用 updateViewConstraints(后简用括号内容:updateConstraints) 和 viewWillLayoutSubviews、viewDidLayoutSubviews(layoutSubviews) 方法
- UIViewController 消失或再次显示时,在 viewWillDisappear / viewWillAppear / viewDidAppear 之后调用 (updateConstraints) 和 (layoutSubviews) 方法
- UIViewController 子视图布局更新时,会调用 (layoutSubviews) 方法
- UIView 首次加载时,在 UIViewController 调用 (layoutSubviews) 时,执行 updateConstraints 和 layoutSubviews 方法
- UIView 自身布局更新或者子视图布局更新时,UIViewController 调用 (layoutSubviews) 时,执行 layoutSubviews 更新布局
- UIButton 和 UITableView 等在首次加载时,会使 UIViewController 多次调用 (layoutSubviews) 方法
屏幕渲染机制
View Drawing Cycle 每隔1/60s,检查视图更新
读取待处理视图列表,主要是视图的修改添加和删除,当列表不为空,在屏幕上显示更新结果
基于约束的布局更新
Traggering Constraint-Based Layout
- setNeedsUpdateConstraints —- 标记下一次绘制 更新约束
- updateConstraintsIfNeed —- 立即更新约束
- updateConstraints — 系统更新的执行方法
基于Frame修改的布局更新
- setNeedsLayout — 标记下一次绘制 更新布局
- layoutIfNeeded — 立即更新布局
- layoutSubviews — 系统调用布局更新的方法
UIViewController 和 UIView 的视图绘制周期
如果我们希望修改了子View的高度约束来修改子View的高度,并看到修改的过程
if (self.hadAddColorView) {
[UIView animateWithDuration:1.f animations:^{
if (self.grayViewHeight.constant == 100.f) {
self.grayViewHeight.constant = 150.f;
} else {
self.grayViewHeight.constant = 100.f;
}
}];
} else {
......
}
可以看到修改的结果是立即执行的,并没有动画的效果,说明子View在修改高度的约束时,frame的重新绘制与当前方法并不在同一绘制周期中进行
if (self.hadAddColorView) {
//如果是直接修改 frame 则默认标记grayView needsLayout 并执行 layoutSubviews 方法
//同时修改了 约束条件 以及 frame属性时,布局系统会重新调用 updateConstraint 将约束布局信息计算为对应 frame 值,调用layoutSubviews 应用计算好的 frame 值。而修改的frame属性值会被忽略
//当视图在使用约束布局时,尽量不要修改该视图的frame值,而应该修改该视图的约束值,防止直接修改的frame值被重新计算的约束更新的frame值覆盖
[UIView animateWithDuration:1.f animations:^{
if (self.grayViewHeight.constant == 100.f) {
//默认标记grayView needsUpdateContraint 及 needsLayout
self.grayViewHeight.constant = 150.f;
} else {
self.grayViewHeight.constant = 100.f;
}
//[self.grayView setNeedsLayout];
[self.grayView layoutIfNeeded];
}];
} else {
......
}
但是只有 grayView 通过动画展示,其他与 grayView 有约束关系的 View 并不执行动画
可以对self.view标记立即更新
if (self.hadAddColorView) {
[UIView animateWithDuration:1.f animations:^{
if (self.grayViewHeight.constant == 100.f) {
self.grayViewHeight.constant = 150.f;
} else {
self.grayViewHeight.constant = 100.f;
}
//[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}];
} else {
......
}
在重写视图绘制方法中修改约束
- (void)updateViewConstraints {
[super updateViewConstraints];
NSLog(@"ViewController updateViewConstraints");
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
NSLog(@"ViewController viewWillLayoutSubviews");
NSLog(@"grayView frame:%@, constant:%f", NSStringFromCGRect(self.grayView.frame), self.grayViewHeight.constant);
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
//
self.grayViewHeight.constant += 100.f;
NSLog(@"ViewController viewDidLayoutSubviews");
NSLog(@"grayView frame:%@, constant:%f", NSStringFromCGRect(self.grayView.frame), self.grayViewHeight.constant);
}
输出为:
ViewController viewDidLoad
ViewController viewWillAppear
ViewController updateViewConstraints
ViewController viewWillLayoutSubviews
grayView frame:{{16, 20}, {240, 150}}, constant:150.000000
ViewController viewDidLayoutSubviews
grayView frame:{{16, 20}, {240, 150}}, constant:250.000000
ViewController viewDidAppear
可以看出在 viewDidLayoutSubviews 中,约束立即更新,但frame没有,打开后可看到子视图已改变,但是子视图布局的改变并没有再次调用 (layoutSubviews) 方法,即在当前绘制周期中修改的约束其实是在下一个绘制周期中执行
如果
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
NSLog(@"ViewController viewDidLayoutSubviews");
self.grayViewHeight.constant += 100.f;
// 会让约束在当前的绘制周期中立即执行
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
NSLog(@"grayView frame:%@, constant:%.0f", NSStringFromCGRect(self.grayView.frame), self.grayViewHeight.constant);
}
输出:
ViewController viewWillLayoutSubviews
grayView frame:{{16, 20}, {240, 123950}}, constant:124050
ViewController viewWillLayoutSubviews
grayView frame:{{16, 20}, {240, 124050}}, constant:124150
ViewController viewWillLayoutSubviews
grayView frame:{{16, 20}, {240, 124150}}, constant:124250
……
// 无限循环,在 viewDidLayoutSubviews 中,遇到立即更新的标记,会立即调用 viewWillLayoutSubviews 开始更新布局