WWDC 2017:高级开发应该掌握的自动布局技巧

构建 app 时使用的自动布局技术,其实就是创建视图与视图之间关系。而约束是创建视图间关系的纽带,帮助咱们的 app 能够适应各类尺寸的屏幕,在应对花样百出的布局需求时游刃有余。

本文已收录至 iOS 成长之路3期·WWDC17内参git

前言

若是你之前从未使用过Autolayout,如今网上已经有不少很优秀的教程,包括往届 WWDC 中 sessions 视频资源均可供查看学习。在本文中将再也不重复基本的使用方法,更多的去介绍一些更加复杂的场景中的应用,本文中技术结合实例使你更容易理解吸取。让咱们一块儿来看看与Autolayout相关的六种技术与应用,这些内容都很是实用,在平常开发中必定会常用到,相信本文必定不会让你失望。github

1. 运行时变换布局(Changing layout at runtime)

一般咱们不只能够在 app 中使用约束来对视图进行简单的定位,也能够组合使用以达到更复杂的效果。咱们今天要讲的第一种技术点,便在运行时改变布局。以下图,在咱们界面的顶部,有一个滑块区域。如今咱们须要一个将滑块视图上移而且最终隐藏的功能。编程

顶部滑块区域

1.1 利用高度约束隐藏视图

一般咱们但愿约束在设置好以后不须要再次调整,尽可能让结构清晰简单。如今咱们来思考一下,从布局的角度使用最简单的方式实现这个功能,通常状况下,咱们把这个区域视图高度缩短至0便可。可是若是咱们真的添加上一个高度约束,而且设置为0。咱们将在 Interface Builder 中发现一些警告。安全

顶部滑块约束存在冲突

1.2 避免冲突

在图片中能够看到布局中的这些红线,这意味着咱们设置的约束存在着一些冲突。之因此出现冲突,是由于咱们设置的这些约束让布局引擎去作了一些不能同时并存的事情。而这个冲突出现是由于咱们设置了高度为0的同时,没法保持足够的高度以知足该控件内部的内容显示。bash

容器

为了解决这个问题,咱们将 slider 和 label 所在的视图放进一个warppingView中,如图中橙色方框。在咱们缩短warppingView的高度时,咱们也要保证warppingView内部子视图的高度,而且知足子视图相关的约束,在启用 clips ToBounds 属性后,超出warppingView内部坐标系范围的内部控件在显示时将被裁剪掉。这样就达到了隐藏视图元素的效果,以下图效果,灰色区域将被裁剪不显示。session

隐藏滑块区域

让咱们来看看在 Xcode 中是如何作到的,咱们须要在运行时控制warppingView的高度,因此咱们将为warppingView手动建立一个高度约束zeroHeightConstraint,在运行时设置zeroHeightConstraint为0,而且在用户点击 Edit 按钮时,激活该约束。这样咱们仍然会和以前同样出现冲突的状况,咱们须要将滑块区域视图底部到warppingView底部边缘的约束禁用,避免了约束冲突,这样warppingView就能够正常缩短高度了。app

禁用底部约束

1.3 实现代码

接下来看看完整代码,在咱们控制器的子类中,咱们持有3个属性:ide

  • warppingView:外部容器视图
  • edgeConstraint:底部边缘的约束
  • zeroHeightConstraint:一个存储0高度约束的属性
@IBOutlet var warppingView: UIView!
@IBOutlet var edgeConstraint: NSLayoutConstraint!
var zeroHeightConstraint : NSLayoutConstraint!
复制代码

咱们建立了按钮点击事件,在响应按钮事件函数中,咱们首先要保证zeroHeightConstraint已被建立。接着咱们还但愿这一个事件让视图能够在显示和隐藏间切换,因此咱们要对一些约束作禁用和激活操做,作完这些就会获得咱们想要的切换效果。函数

@IBAction func toggleDistanceControls(_ sender: Any) {
        if zeroHeightConstraint == nil {
            zeroHeightConstraint = warppingView.heightAnchor.constraint(equalToConstant: 0)
        }
        
        let shouldShow = !edgeConstraint.isActive
        
        if shouldShow {
            zeroHeightConstraint.isActive = false
            edgeConstraint.isActive = true
        }else{
            edgeConstraint.isActive = false
            zeroHeightConstraint.isActive = true
        }
    }
复制代码

须要特别注意的是,在激活一个约束前务必先禁用另一个约束。在这些简单的切换禁用和激活代码,遵照这一点让咱们避免了冲突,若是约束中一旦存在冲突,控制台就会提醒咱们:嘿,我检测到这些约束是互相冲突的😂。例如,咱们激活了zeroHeightConstraint约束,而底部约束edgeConstraint还未被禁用,这个时候咱们就会看到控制台打印出冲突信息。布局

1.4 加入动画

加入这些代码从新运行后你会发现咱们的界面正确显示和隐藏了,可是我还想为这个过程加上动画,让用户能够看到视图切换过程可以提升用户体验。在这里咱们使用UIView animation block来实现动画,UIView animation将捕捉而且动画化整个过程。

UIView.animate(withDuration: 0.25) {
    self.view.layoutIfNeeded()
}
复制代码

这里获得的动画效果,也并非我想要的最终效果,咱们还须要作最后一点调整,可是这不须要修改咱们的代码,咱们只须要将底部边缘的约束:edgeConstraint属性更换成链接到顶部边缘的约束,改为一个底部对齐的效果。整个动画效果发生改变,我确认这就是我须要的最终效果。

禁用顶部约束

具体效果能够查看咱们的Demo(非苹果官方),经过上面这些内容咱们能够知道,怎样经过运行时改变约束来动态调整咱们 app 中的布局。

2. 跟踪触摸手势(Tracking touch)

如今咱们来看看改变布局的另外一种方法,我保证它既简单又炫酷。咱们将用它来跟踪触摸手势。咱们在咱们下图的 app 的中央区域有一张卡片,咱们但愿卡片能随着触摸手势移动,随着靠近边缘的时候,会有一些旋转,一旦你的手离开屏幕,卡片就会弹回屏幕中间。

卡片

2.1 frame 饮水知源

一般一个控件在屏幕上的位置由它的 frame 决定,而 frame 又源起何处呢?

  • Layout engine owns frame。当咱们使用Autolayout并使用约束控制此视图时,布局引擎将会持有此视图的 frame 。
    • Value derived from constraints。frame 的值是从这些约束中计算出来的。
  • transform property offsets from frame。还有另外一个属性会影响视图在屏幕上的位置,那就是 transform ,在 transform 属性源起于 frame 。
  • CGAffineTransform = translation + rotation + scale。经过CGAffineTransform,它能够帮助咱们为视图加入平移 ,旋转和缩放等变换,在从约束中计算出 frame 以后,将其应用在 transform 中。

2.2 加入监听手势

再回到需求上,若是咱们想要中间的卡片随着个人手势移动,那咱们就要加入一个手势识别器,而且拖线链接到代码中,添加监听手势的方法,在该方法中咱们能够访问手势识别器的各类属性。此外还将咱们要移动的卡片也经过拖线建立了属性。

@IBOutlet weak var cardView: UIImageView!
@IBAction func panCard(_ sender: UIPanGestureRecognizer) {}
复制代码

2.3 加入位移和旋转

接下来咱们要经过手势识别器监听用户手势移动,获得位移结果后转换成 transform 应用在cardView上。在这里有transform函数帮咱们进行了位移和轻微的旋转,这个时候,卡片将会随着你的手指移动伴随着轻微的旋转。

func transform(for translation: CGPoint) -> CGAffineTransform {
    let moveBy = CGAffineTransform(translationX:translation.x, y: translation.y)
    let rotation = -sin(translation.x/(cardView.frame.width * 4.0))
    return moveBy.rotated(by: rotation)
}
复制代码

2.4 位置还原

可是当我放开手指时,卡片停留在原位,没有回到屏幕中央,由于咱们并无去重置卡片的 transform 属性。当我再次触摸并移动,咱们会看到它会回到原来的位置,这是由于咱们开始了一个新的位移,新的位移关联的原来的 frame 。总之这不是我想要的效果,我但愿在用户手指离开屏幕后,卡片可以当即回到屏幕中间的位置。咱们能够经过手势识别器的状态来作到这一点。咱们在stateend的时候,将重置 transform 而且加入弹簧动画。加入这部分代码运行 app ,在我松开卡片后它会弹回中间的位置。

@IBAction func panCard(_ sender: UIPanGestureRecognizer) {
    switch sender.state {
    case .changed:
        let translation = sender.translation(in: view)
        cardView.transform = transform(for: translation)
    case .ended:
        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4,
initialSpringVelocity: 1.0, options: [], animations: {
            self.cardView.transform = .identity
        }, completion: nil)
    default:
        break;
    }
}
复制代码

简单的几行代码,实现了一个颇有意思的交互效果,在这些内容里面,咱们能够看到 frame 它不只是经过约束来计算出,也会受到 transform 的影响,视图的 frame 中蕴含多种属性的组合效果。

3. 动态字体(Dynamic type)

Dynamic type是 iOS 中提供了一组文本样式,文本样式包含了标题、副标题、正文等样式,并且用户能够控制这些样式字体大小的技术。在 iPhone 的短信消息中,若是用户喜欢大一点的字体,经过在设置中进行设置后,咱们将看到消息界面会有所变化,字体变大了,消息气泡和输入文本也变大了。日历和其余一些地方也具有相似的功能。

改变字体前

改变字体后

相信这个时候你必定会好奇,如何才能在咱们本身的 app 实现这个功能呢?另外在调整字体大小时,若是不相应地调整咱们的布局,容易形成视图重叠,对用户体验来讲是很是很差的。幸运的是,Autolayout能够很轻松地帮咱们搞定这个问题。

3.1 支持 Dynamic Type

因此赶忙让咱们来看看是怎么实现的吧,打开 IB 界面,选中你要支持 Dynamic Type 的 label ,查看 label 的属性,勾选automatically adjust font,若是你眼睛够敏锐的话,你能看到上面出现了一个警告,缘由是由于automatically adjust font属性生效,该属性要求 label 设置指定的文本样式。这里要将系统默认字体更换为caption one,该样式和默认字体12号大小相对应。

支持Dynamic Yype

3.2 经过 Accessibility Inspector 改变字体大小

设置好这些再从新运行,你会发现和以前并无什么不一样,这是由于咱们尚未改变文本样式的大小,咱们能够在设置中调整字体大小,可是这种方式须要来回切换不够直观,全部咱们用另一种方法,点击顶部导航条Xcode->Open Develop Tool->Accessibility Inspector->target切换至模拟器->选择设置标签,就能够看到修改字体大小的滑块了。这个时候滑动滑块就能看到咱们的 label 字体在实时地改变。若是咱们链接了 iPhone ,咱们也能够将target切换至咱们的 iPhone 。

Accessibility Inspector

3.3 根据字体大小动态调整布局

在下图中能够看到,若是咱们把字体调整到很是大的时候,咱们的 label 就会发生重叠,接下来咱们要解决这个问题。

视图重叠

首先咱们建立了一个文本区域,这个文本区域会随着字体变大而增高,因此咱们只要将底部 label 被限制在底部,顶部 label 被限制在顶部,再在二者之间添加了一个垂直间距约束,使两个标签始终保持足够垂直间距,避免使用固定高度约束,这样 label 的高度会随着字体变大而增高,接着 label 又会将文本区域给撑高。这个时候能够打开Accessibility Inspector来测试改变咱们 app 的字体大小,你会发现咱们的文本区域会随着字体的变化而改变高度。

根据字体大小动态调整布局

在这中间咱们并不须要作不少处理,就能实现这样一个很是实用的功能,特别是当你有一些须要读者阅读文字的的需求 ,相信 Dynamic Type 可以帮助你,让你的 app 更增强大。

4. 安全区(Safe area)

接下来要介绍的内容在以后你可能会频繁使用到,因此必定要搬好小板凳认真看。当你新建了一个控制器,控制器有一个导航条和一个底部标签栏,如何保证你的内容主体不被导航条和标签栏遮挡?可能你已经据说过在 iOS 11 上有了新的 layout guide ,称之为Safe Area Layout Guide

Safe Area

4.1 Safe Area Layout Guide 更易使用

这是UIView的新特性,它适用于自动布局,它是夹在导航条和标签栏之间的一个矩形,在这个矩形区域中你能够放心地为你的视图添加约束。在这以前,你可能不得不使用UIViewControllerTop Layer GuideBottom Layer Guide,如今在Safe Area中这些都已经统统被丢弃了。

使用Safe Area

Safe Area Layout Guide使用起来更加简单,也更容易理解,如同字面意思,能够安全的让你的视图安全地呆在导航条和标签栏中间,不被遮挡,无论是尺寸的变化和屏幕旋转,它都会自动作相应地调整。

使用Safe Area 横屏

4.2 如何使用 Safe Area Layout Guide

Safe Area也适用在 tvOS 上。若是要将你的 app 和内容放到 tvOS 上,你可能会遇到各类各样的尺寸的屏幕,在某些状况以下图,咱们顶部的标题太靠近顶部边缘,可能所以被遮挡掉一部分。

顶部标题被截取

这个时候咱们要调整咱们的内容,让它处于Safe Area之中。Safe Area表明storyboard中的这块浅绿色的区域,你只要将你视图中的约束设置到Safe Area中,那它就安全了。

浅绿色的安全区域

而后用一张的美丽背景图像填充剩余视图空间,妈妈在也不用担忧咱们的内容被导航条和标签栏挡住了。以下图,它们只会听话地呆在深色矩形方框内。

添加背景

4.3 开启 Safe Area Layout Guide

开启Safe Area Layout Guide也十分简单,打开咱们的storyboard,进入 file inspector 标签页,而后找到Use Safe Area Layout Guides而且勾选上。你会发现每一个控制器中都会出现一个Safe Area视图,而后你就能够像其余视图同样,将约束连向它。

开启Safe Area

Safe Area Layout Guide是 UIView 的新特性,在以前的版本中顶部和底部的Layout Guides之间的矩形区域将与新的Safe Area相匹配,他们能够互相转换,若是在 iOS 11 的故事板中启用Safe Area,在你选中afe Area Layout Guides勾选框时 Xcode 将会自动升级你的约束。总而言之,在 Xcode 9 的故事板中使用Safe Area Layout Guide,将向下兼容 iOS 老版本的。

5. 比例定位(Proportional positioning)

接下来咱们要谈一谈,关于如何将一个视图定位在其 superview 的布局技术,咱们将其称之为 Proportional positioning ,即按 比例定位 。在安卓的布局技术中也有相似的功能,它的应用面普遍且实用,相信将来的开发中必定会频繁使用到。

5.1 比例布局

假设如今我有一个需求,要将咱们 app 中的卡片高度定位在其 superview 高度的70%。也许你会有几种方式能够实现上述需求,可是如今我要用一个最直接的方式来实现它,即是我如今要介绍得的使用 spacerview 的方法。

高度为superview高度的70%

从对象库拖出一个视图,只是一个普通的UIView。为它添加约束后设置隐藏,这样就不会渲染它,让它作一个安静的美男子,这样它就成为你须要定位的视图的参照物。并且这种技术也能够组合使用,灵活搭配,这里有另外一个例子,我有一个场景,要遵照1/5,2/5和2/5的比例,而后他们以这些比例填充满整个屏幕。下面让咱们来看看如何作到的。

比例1:2:2

5.2 构建 SpacerView

以下图中咱们已经有一个基本的布局。我有一个 label 和一个 image ,已经添加了基本约束。 当我选中它们,若是你仔细看,你会注意到它左边和右边的约束是蓝色的,但顶部和底部是红色的。这意味着咱们还须要添加一些约束来定位。不管什么时候在 Interface Builder 画布中看到红色,那只多是两种状况,要么你的约束太少,位置是不肯定的,或者设置了太多的约束,其中一部分是冲突的。

出现冲突

我知道是由于我没有对垂直方向位置进行固定。因此接下来咱们要经过建立 spacerview 来实现。拖一个UIView出来。首先咱们将其隐藏,这样不会浪费性能进行绘制,咱们为其添加好上方、左侧以及宽度的约束后,咱们尚未设置其高度约束,咱们要为其设置等高约束,使其高度与 superview 高度相,以下图效果。

spacerview

接着让咱们查看等高约束的属性,修改比例为70%,设置成功后你会发现spacerview的高度已经缩减了,下一步设置 Second Item 即比例参考对象视图为 Safe Area,这样咱们的spacerview就已经设置好了。

设置为 Safe Area 70%高度

5.3 对齐到 Baseline

如今咱们要将咱们卡片视图底部与spacerview底部对齐,因此咱们添加了底部对齐的约束,若是我想要是spacerview与咱们的卡片文案的baseline对齐怎么办?选中约束后转到属性检查器,选择 FirstItem 选项,选中First Baseline便可。

对齐baseline

从新运行后获得了我想要的效果。

占比 70%

因此当你须要在 Interface Builder 中使用这个比例定位技术时,使用spacerview可以帮助你达到指望。可是必定要将这些视图标记为隐藏,使它们不会被渲染,但又能协助你进行布局,使你可以定位你的内容。若是你是用编程方式进行布局,可使用UILayout Guide来完成,你能够将其用做等效于spacerviews

6. Stack view 自适应布局(Stack view adaptive layout)

让咱们一块儿来看看最后一种咱们要布局的视图,以下图,你能看到 app 中展现了一个自适应布局的页面,上方是一个4x4的网格排列,底部有一个 label 。

竖屏效果

当我旋转手机的时候,出现了一些不同的东西。它仍然会显示一个4x4的网格,但它有一个文本视图出如今右边的位置。

横屏效果

6.1 竖屏布局

这一切是怎么作到的呢?让咱们来看看如何对 Interface Builder 中的 stackview 进行自适应布局。首先看下图中最外层是一个垂直的 stackview ,从上到下分红三行,第一行和第二行都是包含两张图片的水平 stackview ,第三行是一个 label 。就如你看到的,他们高度是相等的,咱们能够经过 AlignmentDistribution,和 Spacing 等属性来进行调整,以达到你想要的布局。 stackview 有一个很是赞的地方,就是它能帮你管理被包含的视图的约束,这样你只要添加不多的约束。

三行布局

接下来让咱们选中全部的 stackview ,在Distribution选项中选择fill equally 实现平均分布,在Spacing选项中咱们能够手动输入咱们想要的间距,另外系统也提供了标准间距选项给咱们,点击输入框右边的倒三角就会出现一个Use Standard Value选项,直接选中便可。

间距属性

下一步我要确保这些图像是正方形的,咱们直接选中第一张图片,为其添加一个宽高比为1:1的约束,添加后你会发现出现一些冲突,这是由于在知足填充满整个屏幕和三行平均分布的同时,没法保证图片比例达到1:1。

宽高比例约束冲突

因此咱们要作一些改变,咱们将 stackview 到底部的固定约束修改为大于等于,这样出现的冲突就解决了,也达到了我想要的效果。

调整底部约束

6.2 横屏布局

当咱们将设备旋转到横屏状态,咱们预期的效果是在右边有一个 textview ,而底部并无 label ,为了更接近咱们预期效果咱们须要把底部的 label 先隐藏。咱们要如何才能作到在竖屏中显示,在横屏中隐藏呢?

横屏

在全新的 Xcode9 中的隐藏属性,能够为不一样size class分别设置显示或隐藏。转到 label 的hidden属性,你会发现勾选按钮左边有一个加号,它让这一切变得轻松简单。点击后在弹出的界面中Width选择any,在横屏的时候,Height选择compact,由于在横屏的时候它的高度是紧凑的。

隐藏属性变量

作完这些点击add variation,而且在hidden属性下面找到刚刚设置的隐藏属性而且勾选中它,你会发现 label 被隐藏了,若是你切换成竖屏,又会显示出来。

接下来继续添加一个 textview ,为了作到这点,咱们要在最外层套一个水平排列的 stackview ,而后将 textview 加入 stackview 中,而且为新建的 stackview 添加约束,其中底部约束就如同以前设置为大于等于。这样就获得了咱们横屏中须要的效果。

加入textView

当咱们切换到竖屏时 textview 仍然显示了,咱们要在竖屏时隐藏它,就像以前同样转到隐藏菜单,并添加一个变量,Width选择anyHeight选择Regular,而后将其标记为隐藏,这样在竖屏时, textview 就再也不显示了,就此达到了咱们指望的效果。

竖屏时须要隐藏 textView

在使用 stackview 的时候,咱们可使用AlignmentDistributionSpacing这些属性帮助咱们布局。还有嵌套使用,只须要加入不多的约束,咱们仅仅须要在你对宽高比例有要求的时候,经过宽高比例约束来得到咱们想要的比例。使人惊喜的是,Xcode 9 中的隐藏属性是可分级的,它很是适合与 stackview 搭配使用,并且随着size class的变化的隐藏属性是向下兼容的。

总结

到这里咱们已经看完了Autolayout相关的的六种技术,在你构建 app 的时候有了更多的布局手段,这些技术可以使你的界面看起来很是美观,结构清晰,而且自适应布局,在平常开发中会常用到。我已经火烧眉毛地想看到更多人使用这些技术了。

Demo

GitHub:FindMyDates

参考

相关文章
相关标签/搜索