iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画

首先感谢众多网友的支持,最近我实在是事情太多,所以没有写太多。不过看到大家的反馈和评价,我还是要坚持挤出时间给大家分享我的经验。如果你对我写的东西有任何建议、意见或者疑问,请到我的CSDN博客留言:

http://blog.csdn.net/pucker

好了,言归正传。本系列的前几篇文章讲解了自动布局的原理,以及如何添加约束。这篇文章主要介绍以下内容:

  • 某些用户控件具有自身内容尺寸约束
  • 使用视图调试工具在运行时查看和调试程序界面视图层次、尺寸和自动布局约束
  • 创建约束的对象关联
  • 通过修改约束的常量值、删除旧约束添加新约束、设置约束激活属性、设置约束优先级等方式,实现视图的布局更新
  • 使用动画更新界面布局
  • 设置带有自身内容控件的抗压缩与抗拉抻优先级

下面结合一个用户登录界面的例子来讲解。首先请下载初始项目:

http://yunpan.cn/cQDIbjtf98zzV (提取码:3d6b)

解压缩并使用Xcode打开该项目,选择任意一个iPhone模拟器,编译项目并运行,如图所示。

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第1张图片

一、自身内容尺寸约束

回到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)以开启界面层次调试工具。

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第2张图片

此时Xcode左侧会列出视图层次、视图类型(包括系统私有类型)与布局约束。中间区域显示视图的详细样式、尺寸、层次等,可以在空白处拖动鼠标以不同视角观察和调试界面。右侧会根据所选内容显示其不同属性。

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第3张图片

选中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语言进行描述。至于什么是Hug和CompressionResistance,在后面会讲到抗挤压与抗拉抻效果。另外我们还打印出了约束的优先级,在后面也会讲解优先级的作用。

(请思考,可否将上面的代码不放在viewDidAppear:方法中,而是放在viewDidLoad方法中执行?为什么?)

如果开发者显式给出了宽度和高度约束,则默认情况下,以显式约束为准。选中Head Image View并添加宽度120点、高度120点的约束,重新编译运行程序,则视图调试工具显示其布局约束为:

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第4张图片

其中的自身内容尺寸约束为灰色,表示不起作用。同时控制台输出为:

<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有一个不太好的用户体验,那就是在输入用户名和密码时,键盘会遮挡住文本输入框和登录按钮:

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第5张图片

我们需要在键盘弹出或者收回时更新界面布局,主要有以下几种方式来更新界面布局:

  • 修改约束的常量值
  • 设置约束激活属性(删除旧约束并添加新约束)
  • 调整约束的优先级

当只需要平移视图的位置就能解决问题时,可以使用第一种方法直接修改某一约束的常量值。这种方式最简单最高效,但是不能解决所有问题,这时可以使用后两种方式。

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方法中移除键盘事件监听。编译运行程序,点击文本输入框,这一次键盘弹出后由于文本控件上移,所有界面控件的位置都上移了,就不会被键盘挡住了。

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第6张图片

由于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键删除该约束。

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第7张图片

然后选中文本控件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:

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第8张图片

改完之后Interface Builder会出现错误提示,因为我们刚刚添加的这两个约束是彼此冲突的(label.centerY = superView.centerY && label.centerY = 0.6 * superView.centerY,这不可能同时满足)。

点击视图层次窗口上方的红色箭头,Interface Builder会列出上述两个彼此冲突的约束。选中某个约束,右侧尺寸窗口会列出该约束的详细信息。我们选中Multiplier为0.6的那个约束,然后在右侧尺寸窗口下方取消勾选Installed选框。

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第9张图片

Installed选框的值就对应约束对象的active属性的值,即表示该约束是否为激活状态,勾选表示激活状态(生效状态,active属性为YES),不勾选表示未激活状态(无效状态,active属性为NO)。现在Multiplier为0.6的那个约束不再生效,因此就不存在约束冲突了。

然后按照上文中介绍的方法,添加上面两个约束的对象关联,Multiplier为1的约束命名为labelCenterYNormalCons,Multiplier为0.6的约束命名为labelCenterYKeyboardCons,且Storage设置为Strong:

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第10张图片

这是由于需要向视图动态添加或者移除约束,因此需要确保使用强引用确保约束对象不会被回收。

然后修改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:方法。编译运行如图:

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第11张图片

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];
    }];

重新编译运行,由于使用了动画来重新对界面布局,变化的过程就显得非常自然了。

三、自身内容尺寸约束的抗挤压与抗拉抻效果

前面讲了,某些控件具有自身内容尺寸约束,也就是根据自身内容的大小添加必要的约束。我们不妨将这类控件看作是一个弹簧。

弹簧的原始状态

弹簧会有自身固有长度,当有外力作用时,弹簧会抵抗外力作用,尽量接近固有长度。

  • 抗拉抻:当外力拉长弹簧时,弹簧长度大于固有长度,且产生向内收的力阻止外力拉抻,且尽量维持长度接近自身固有长度。
    弹簧的拉抻状态
  • 抗挤压:当外力挤压弹簧时,弹簧长度小于固有长度,且产生向外顶的力阻止外力挤压,且尽量维持长度接近自身固有长度。
    弹簧的压缩状态

ViewController类的viewDidAppear:方法打印出了头像图片视图的所有约束:

<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

对于自身内容尺寸约束,Hug值表示抗拉抻优先级,CompressionResistance值表示抗压缩优先级。Hug值越高越难被拉抻,CompressionResistance值越高越难被压缩。

由于自身内容是运行时动态变化的,我们可以通过这两个优先级来决定控件是否允许在某些条件下被压缩、拉抻。对于上面的输出结果,图片本身大小133*133,抗压优先级CompressionResistance为750,显式宽度约束为120优先级为1000。由于显示宽度优先级大于抗压优先级,所以最终图片宽度为120。但是,当我们降低显式宽度约束的优先级,令其小于抗压优先级时,以自身宽度133为主。在Main.storyboard中选中用户头像,双击图片下方的显式宽度约束,将其优先级Priority设置为500。

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第12张图片

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第13张图片

注意到上图中红圈部分,头像宽度变为133,高度维持120。这说明当显式约束优先级高于抗压抗拉优先级时,以显式约束为准;当显式约束优先级小于抗压抗拉优先级时,以自身内容约束为准。

再举一个例子,当我们输入用户名和密码,然后点击程序的登录按钮后,下方的两个文本控件会显示出输入的用户名和密码:

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第14张图片

两个文本控件不超过父视图的两边,且两者间具有水平间距互不覆盖。当输入的用户名和密码比较短时,两者都能完整显示。但是当内容较长时,我们发现左侧文本控件被截断了。如果我们希望保持左侧文本框完整,必要时截断右侧文本框,则可以令左侧文本框的抗压优先级高于右侧文本框抗压优先级。可以在IB中直接设置抗压抗拉优先级。在Main.storyboard的左侧视图结构窗口中,选中左侧文本控件User Name Label,在右侧尺寸窗口的Content Compression Resistance Priority部分,将Horizontal的值改为751。重新编译运行,输入用户名和密码,现在左侧文本控件完整,右侧文本控件被截断。

iOS 8 Auto Layout界面自动布局系列5-自身内容尺寸约束、修改约束、布局动画_第15张图片

这是由于左侧抗压优先级高于右侧抗压优先级的缘故。

当然我们也可以使用代码来设置水平和垂直抗压抗拉优先级,方法是调用UIView的如下几个方法:

- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis; - (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis; - (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis; - (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis;

在此就不举例赘述了。

四、总结

本篇文章讲解的内容比较繁杂,程序最终的代码如下。

http://yunpan.cn/cQ4snekTsfyMC (提取码:57f6)

如果你有关于Autolayout的任何问题,请在我的CSDN博客留言。

在下一篇文章中,我想讲一讲iOS 8中关于设备适配、Adaptive Layout与Size Class的使用,敬请期待。

你可能感兴趣的:(ios,layout,自动,布局,约束)