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


原文链接:http://www.itnose.net/detail/6309814.html

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

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

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

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

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

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

    三、自身内容尺寸的弹簧效果

    未完待续。。。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值