首先感谢众多网友的支持,最近我实在是事情太多,所以没有写太多。不过看到大家的反馈和评价,我还是要坚持挤出时间给大家分享我的经验。如果你对我写的东西有任何建议、意见或者疑问,请到我的博客留言:
好了,言归正传。本系列的前几篇文章讲解了自动布局的原理,以及如何添加约束。这篇文章主要介绍以下内容:
- 某些用户控件具有自身内容尺寸约束
- 使用视图调试工具在运行时查看和调试程序界面视图层次、尺寸和自动布局约束
- 创建约束的对象关联
- 通过修改约束的常量值、删除旧约束添加新约束、设置约束激活属性、设置约束优先级等方式,实现视图的布局更新
- 使用动画更新界面布局
下面结合一个用户登录界面的例子来讲解。首先请下载初始项目:
http://yunpan.cn/cQDIbjtf98zzV (提取码:3d6b)
解压缩并使用Xcode打开该项目,选择任意一个iPhone模拟器,编译项目并运行,如图所示。
一、自身内容尺寸约束
回到Xcode打开Main.storyboard,选中用户头像图片视图Head Image View,并打开尺寸窗口(Size Inspector,快捷键??5)查看其布局约束。
可以看到该图片视图当前具有2个约束:
- 水平中心点与其父视图水平中心点对齐(确定图片水平位置x)
- 底部与下方文本控件顶部相隔20点的距离(已知下方文本控件的垂直位置是确定的,因此也就确定了图片垂直位置y)
等等,这里貌似有问题。细心的读者可能会发问了,本系列的第一篇文章明确说过,要确定一个视图的精确位置,至少需要4个布局约束(以确定水平位置x、垂直位置y、宽度w和高度h)。可现有的2个约束仅能确定x和y,缺少必要的信息来确定w和h。然而此时Interface Builder并没有提示缺少约束的错误(如果真的缺少约束,则Interface Builder会显示红色错误圆圈,并提示Missing Constraints),并且程序运行正常且没有报错,这是怎么回事呢?
请注意,某些用来展现内容的用户控件,例如文本控件UILabel、按钮UIButton、图片视图UIImageView等,它们具有自身内容尺寸(Intrinsic Content Size),此类用户控件会根据自身内容尺寸添加布局约束。也就是说,如果开发者没有显式给出其宽度或者高度约束,则其自动添加的自身内容约束将会起作用。因此看似“缺失”约束,实际上并非如此。
对于UIImageView,其自身内容尺寸就是图片(1倍图)的尺寸。打开Images.xcassets,选中head中的1x图,在属性窗口(Attribute Inspector)中可以看到其尺寸为133px*133px。
我们不妨使用Xcode提供的界面层次调试工具在运行时动态查看视图层次、尺寸以及布局约束等信息。如果当前没有运行程序,请编译运行,然后打开调试导航窗口(Debug Navigator),点击进程查看选项按钮(Process View Option),选择界面层次(View UI Hierarchy)以开启界面层次调试工具。
此时Xcode左侧会列出视图层次、视图类型(包括系统私有类型)与布局约束。中间区域显示视图的详细样式、尺寸、层次等,可以在空白处拖动鼠标以不同视角观察和调试界面。右侧会根据所选内容显示其不同属性。
选中UIImageView,在右侧打开尺寸窗口,在Auto Layout区域可以看到4个黑色的约束,其中两个就定义了宽度w为133点,高度h为133点,并且后面加了(content size)表示此约束是自身内容尺寸约束。视图调试工具对解决界面自动布局问题很有帮助,当出现问题却又不知什么原因的时候,不妨用该工具调试。
当然,我们也可以使用代码打印出某个视图的自动布局约束,这也是常用的调试手段。在Main.storyboard中选中Head Image View并在属性窗口中设置其Tag为99,然后在ViewController.m中添加viewDidAppear:方法:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UIView* headImageView = [self.view viewWithTag:99];
for (NSLayoutConstraint* eachCon in headImageView.constraints)
{
NSLog(@"\n%@\nPriority:%f", eachCon, eachCon.priority);
}
}
运行后的输出为:
<NSContentSizeLayoutConstraint:0x7aeda9e0 H:[head(133)] Hug:251 CompressionResistance:750 (Names: head:0x7af84130 )>
Priority:1000.000000
<NSContentSizeLayoutConstraint:0x7aedab30 V:[head(133)] Hug:251 CompressionResistance:750 (Names: head:0x7af84130 )>
Priority:1000.000000
可以看到打印的每条约束都使用了VFL语言进行描述。
(请思考,可否将上面的代码不放在viewDidAppear:方法中,而是放在viewDidLoad方法中执行?为什么?)
如果开发者显式给出了宽度和高度约束,则以显式约束为准。选中Head Image View并添加宽度120点、高度120点的约束,重新编译运行程序,则视图调试工具显示其布局约束为:
其中的自身内容尺寸约束为灰色,表示不起作用。同时控制台输出为:
<NSLayoutConstraint:0x7c189ac0 H:[head(120)] (Names: head:0x7c1897a0 )>
Priority:1000.000000
<NSLayoutConstraint:0x7c189af0 V:[head(120)] (Names: head:0x7c1897a0 )>
Priority:1000.000000
<NSContentSizeLayoutConstraint:0x7bea62a0 H:[head(133)] Hug:251 CompressionResistance:750 (Names: head:0x7c1897a0 )>
Priority:1000.000000
<NSContentSizeLayoutConstraint:0x7bea63f0 V:[head(133)] Hug:251 CompressionResistance:750 (Names: head:0x7c1897a0 )>
Priority:1000.000000
二、创建约束的对象关联并修改约束
我们这个用户登录的app有一个不太好的用户体验,那就是在输入用户名和密码时,键盘会遮挡住文本输入框和登录按钮:
我们需要在键盘弹出或者收回时更新界面布局,主要有以下几种方式来更新界面布局:
- 修改约束的常量值
- 设置约束激活属性(删除旧约束并添加新约束)
- 调整约束的优先级
当只需要平移视图的位置就能解决问题时,可以使用第一种方法直接修改某一约束的常量值。这种方式最简单最高效,但是不能解决所有问题,这时可以使用后两种方式。
1 . 修改约束常量值
对于这个App来说,所有控件的垂直位置都是基于位于中央的文本控件的垂直位置而定。我们打算在键盘未弹出时,文本控件顶部距离Top Layout Guide的垂直间距为250(label.top = 250);在键盘弹出时,将该间距缩小为0(label.top = 0)。
Interface Builder不仅允许我们创建视图对象的IBOutlet对象关联,还可以创建约束对象的对象关联,这样就能通过代码来访问并修改某个约束。
回到Xcode打开Main.storyboard,选中文本控件User Name and Pwd Label,在右侧的尺寸窗口中单击顶部约束蓝线,并双击下方的Top Space to: Top Layout Guide约束:
此时左侧的项目窗口会高亮选中该约束。切换到助手编辑器,确认右侧窗口中打开的是ViewController.m,然后选中该约束并按住?键拖拽到右侧ViewController类的类扩展区域,在弹出窗口中将其命名为userNamePwdLabelTopCons,点击Connect按钮就创建了约束对象的对象关联,其步骤类似于创建视图的对象关联。
接下来ViewController类需要响应键盘弹出和收回事件,向ViewController类的viewDidLoad方法中添加如下代码:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
UIKeyboardWillShowNotification与UIKeyboardWillHideNotification这两个通知消息会在键盘即将弹出以及键盘即将收回时抛出,我们可以在keyboardWillShow:和keyboardWillHide:这两个方法中修改userNamePwdLabelTopCons约束。
注意,对于约束的如下几个重要属性:
/* accessors
firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant
*/
@property (readonly, assign) id firstItem;
@property (readonly) NSLayoutAttribute firstAttribute;
@property (readonly) NSLayoutRelation relation;
@property (readonly, assign) id secondItem;
@property (readonly) NSLayoutAttribute secondAttribute;
@property (readonly) CGFloat multiplier;
/* Unlike the other properties, the constant may be modified after constraint creation. Setting the constant on an existing constraint performs much better than removing the constraint and adding a new one that‘s just like the old but for having a new constant.
*/
@property CGFloat constant;
当使用代码来修改约束时,只能修改约束的常量值constant。一旦创建了约束,其他只读属性都是无法修改的,特别要注意的是比例系数multiplier也是只读的。
然后向ViewController类添加如下代码:
- (void)keyboardWillShow:(NSNotification *)notification
{
//在键盘弹出时,文本控件顶部距离Top Layout Guide的垂直间距为0
self.userNamePwdLabelTopCons.constant = 0.0f;
}
- (void)keyboardWillHide:(NSNotification *)notification
{
//键盘未弹出时,文本控件顶部距离Top Layout Guide的垂直间距为250
self.userNamePwdLabelTopCons.constant = 250.0f;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
别忘记在dealloc方法中移除键盘事件监听。编译运行程序,点击文本输入框,这一次键盘弹出后由于文本控件上移,所有界面控件的位置都上移了,就不会被键盘挡住了。
由于ViewController类重写了触屏方法,并取消了文本输入框的第一响应者状态,因此此时点击文本输入框之外的区域就会收起键盘,这样就会恢复到原始布局状态。
#pragma mark - Touch event Handler
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
[self.userNameTextField resignFirstResponder];
[self.userPwdTextField resignFirstResponder];
}
2 . 修改约束激活属性,或者删除旧约束并添加新约束
现在我们打算这样布局界面:在键盘未弹出时,文本控件垂直中心与其父视图垂直中心相同(label.centerY = superView.centerY);在键盘弹出时,文本控件垂直中心是其父视图垂直中心的0.6倍(label.centerY = 0.6 * superView.centerY)。
对于刚才的例子,我们可以通过修改某个约束的常量值来解决问题。但是这次不一样了,比例系数是只读的,在约束创建之后就不可以修改。所以对于这种情况,我们就不能对某个约束进行修改,而是需要把不需要的约束去掉,然后添加一个新的约束。
在Main.storyboard中,在左侧视图层次窗口中选中文本控件距离顶部Top Layout Guide的约束Vertical Space - (250) - User Name and Pwd Label - Top Layout Guide,然后按下Delete键删除该约束。
然后选中文本控件User Name and Pwd Label,点击Align菜单,勾选Vertical Center in Container并取值为0,点击Add 1 Constraint按钮。这样就使得文本控件垂直居中。
重复上图中的步骤,再次创建一个文本控件垂直居中的约束。选中文本控件User Name and Pwd Label,在右侧尺寸窗口中单击垂直中心约束蓝线,下方会列出刚才我们创建的两个垂直居中约束。
双击上方的Align Center Y to: Superview约束,确保First Item为User Name and Pwd Label.Center Y,Second Item为SuperView.Center Y。如果不是,则点击First Item或者Second Item下拉菜单,选中Reverse First And Second Item,对调First Item与Second Item(本系列第二篇文章介绍过的相对关系与反函数)。然后在右侧尺寸窗口中将Multiplier的值由1改为0.6:
改完之后Interface Builder会出现错误提示,因为我们刚刚添加的这两个约束是彼此冲突的(label.centerY = superView.centerY && label.centerY = 0.6 * superView.centerY,这不可能同时满足)。
点击视图层次窗口上方的红色箭头,Interface Builder会列出上述两个彼此冲突的约束。选中某个约束,右侧尺寸窗口会列出该约束的详细信息。我们选中Multiplier为0.6的那个约束,然后在右侧尺寸窗口下方取消勾选Installed选框。
Installed选框的值就对应约束对象的active属性的值,即表示该约束是否为激活状态,勾选表示激活状态(生效状态,active属性为YES),不勾选表示未激活状态(无效状态,active属性为NO)。现在Multiplier为0.6的那个约束不再生效,因此就不存在约束冲突了。
然后按照上文中介绍的方法,添加上面两个约束的对象关联,Multiplier为1的约束命名为labelCenterYNormalCons,Multiplier为0.6的约束命名为labelCenterYKeyboardCons,且Storage设置为Strong:
这是由于需要向视图动态添加或者移除约束,因此需要确保使用强引用确保约束对象不会被回收。
然后修改keyboardWillShow:与keyboardWillHide:方法:
- (void)keyboardWillShow:(NSNotification *)notification
{
self.labelCenterYNormalCons.active = NO;
self.labelCenterYKeyboardCons.active = YES;
}
- (void)keyboardWillHide:(NSNotification *)notification
{
self.labelCenterYKeyboardCons.active = NO;
self.labelCenterYNormalCons.active = YES;
}
注意,尽量先设置需要将active置为NO的约束,然后再设置需要将active置为YES的约束,如果颠倒上面两条语句的话,可能会引起运行时约束错误。另外由于active属性是iOS 8 SDK新添加的属性,对于iOS 6与iOS 7来说,需要调用addConstraint:与removeConstraint:方法。编译运行如图:
3 . 调整不同约束的优先级
刚才的例子是通过调整不同约束的active属性(删旧添新)来实现界面布局调整。另外还有一种方式也很重要,就是下面说的调整不同约束的优先级。
每个约束都会具有优先级(Priority),对应NSLayoutConstraint对象的priority属性:
@interface NSLayoutConstraint : NSObject
......
/* If a constraint‘s priority level is less than UILayoutPriorityRequired, then it is optional. Higher priority constraints are met before lower priority constraints.
Constraint satisfaction is not all or nothing. If a constraint ‘a == b‘ is optional, that means we will attempt to minimize ‘abs(a-b)‘.
This property may only be modified as part of initial set up. An exception will be thrown if it is set after a constraint has been added to a view.
*/
@property UILayoutPriority priority;
......
@end
优先级是一个浮点值,取值范围从1(最低)到1000(最高)。一些常用的优先级值被定义了别名:
typedef float UILayoutPriority;
static const UILayoutPriority UILayoutPriorityRequired NS_AVAILABLE_IOS(6_0) = 1000; // A required constraint. Do not exceed this.
static const UILayoutPriority UILayoutPriorityDefaultHigh NS_AVAILABLE_IOS(6_0) = 750; // This is the priority level with which a button resists compressing its content.
static const UILayoutPriority UILayoutPriorityDefaultLow NS_AVAILABLE_IOS(6_0) = 250; // This is the priority level at which a button hugs its contents horizontally.
static const UILayoutPriority UILayoutPriorityFittingSizeLevel NS_AVAILABLE_IOS(6_0) = 50;
具有优先级1000(UILayoutPriorityRequired)的约束为强制约束(Required Constraint),也就是必须要满足的约束;优先级小于1000的约束为可选约束(Optional Constraint)。默认创建的是强制约束。
在使用自动布局后,某个视图的具体位置和尺寸可能由多个约束来共同决定。这些约束会按照优先级从高到低的顺序来对视图进行布局,也就是视图会优先满足优先级高的约束,然后满足优先级低的约束。
对于上面的例子,我们曾经创建了两个相互冲突的约束,即label.centerY = superView.centerY && label.centerY = 0.6 * superView.centerY。之所以出现冲突,是因为这两者的优先级相同,都是1000。但是如果将其中一个的优先级降低,那么就不会存在冲突,因为优先级高的那个约束会优先起作用。
打开Main.storyboard,将Multiplier为0.6的约束的Installed选框勾上,此时再次出现布局冲突。接着在右侧尺寸窗口中将其Priority设置为250,此时布局冲突消失,同时注意到界面中代表该约束的蓝线变为虚线,表示这是一个优先级较低的可选约束。
以同样的方式,设置另外的Multiplier为1的垂直居中约束的Priority为750。
然后将keyboardWillShow:与keyboardWillHide:方法修改如下:
- (void)keyboardWillShow:(NSNotification *)notification
{
self.labelCenterYNormalCons.priority = UILayoutPriorityDefaultLow;
self.labelCenterYKeyboardCons.priority = UILayoutPriorityDefaultHigh;
}
- (void)keyboardWillHide:(NSNotification *)notification
{
self.labelCenterYKeyboardCons.priority = UILayoutPriorityDefaultLow;
self.labelCenterYNormalCons.priority = UILayoutPriorityDefaultHigh;
}
重新编译运行,效果同上。
需要注意的是,只能修改可选约束的优先级,也就是说:
- 不允许将优先级由小于1000的值改为1000
- 不允许将优先级由1000修改为小于1000的值
例如,如果将优先级由250修改为1000,则会抛出异常:
*** Terminating app due to uncaught exception ‘NSInternalInconsistencyException‘, reason: ‘Mutating a priority from required to not on an installed constraint (or vice-versa) is not supported. You passed priority 1000 and the existing priority was 250.‘
这就是为什么在storyboard中要先将两者的约束分别设置为750和250的原因。
4 . 使用动画更新界面布局
由于修改的约束会立即生效,因此当键盘弹出或者收回时,控件位置的变化显得非常生硬。我们不妨使用动画来更新界面布局,方法是调用UIView的静态动画方法,在动画块代码体中向需要更新约束的视图对象调用layoutIfNeeded方法即可。分别向keyboardWillShow:和keyboardWillHide:方法的最后插入如下代码:
[UIView animateWithDuration:0.25f animations:^
{
[self.view layoutIfNeeded];
}];
重新编译运行,由于使用了动画来重新对界面布局,变化的过程就显得非常自然了。
三、自身内容尺寸的弹簧效果
未完待续。。。
版权声明:本文为博主原创文章,未经博主允许不得转载。