译者的话:
本文是自适应布局的巩固篇,所以对布局约束的添加操作步骤等没有详细的说明。如果看着吃力的话请先移步Swift自适应布局(Adaptive Layout)教程

Apple从iOS6加入了Auto Layout后开始就比较委婉的开始鼓励、建议开发者使用自适应布局,但是到目前为止,我感觉大多数开发者一直在回避这个问题,不管是不是由于历史原因造成的,至少他们在心底还坚守着固定布局的老传统思想。

随着iPhone6、iPhone6 Plus的到来,使用自适应布局更是迫在眉睫的事,固定布局的老传统思想脆弱的不堪一击。现在的iPhone有4种尺寸,如果算上iPad,现在Apple的iOS设备有5种尺寸。我们在准备使用自适应布局设计应用界面之前,可以把这5种尺寸划分为3种分辨率和屏幕方向,这样在设计时分类会更加清晰一些。

我们先来看一张自适应布局的效果图:

图片

当你们学习完这篇文章后,你们应该会比较自如的使用 storyboardconstaintssize classes 这三个Apple在Xcode里提供的工具,去探索和构建巧妙的自适应布局。

Storyboards

在Xcode中,storyboard是一个可以让我们对应用界面进行可视化布局的工具,你首先可以在storyboard文件中看到一个或若干个iOS设备屏幕大小的布局区,然后你可以从组件库(Object Library)中拖拽组件到屏幕布局区中进行布局(比如按钮、图片、文本框、labels等),你还可以定义屏幕布局区之间的连接关系。

用Xcode的术语来说,人们可以看到、触碰到或以其他方式(按钮、图片、文本框、labels等)进行交互的用户界面被称为views。屏幕中包含和管理这些views的容器称为view controller

当我们在storyboard中添加一个view controller后,我们所看到的并不是一个我们熟知的屏幕尺寸,而是一个600X600的正方形:

图片

从上图我们可以很明显的看出,storyboard中显示的屏幕尺寸不是实际设备的尺寸。Apple这样做的目的是将屏幕尺寸进行了抽象化,也就是说你可以将这个正方形的屏幕看成iphone4的屏幕,也可以将它看成iphone6的屏幕。

模拟器的尺寸

当你习惯了600X600的屏幕后,你可能会用着很爽,但是有些时候,我们也需要将它改成实际的屏幕尺寸来设计一些东西。

我们可以很方便的在Xcode中改变view controller的模拟屏幕尺寸和屏幕方向:

图片

布局约束

介绍布局约束的最好、最直观的方法就是向你们展示一个示例。

首先我们将storyboard中的屏幕布局区域的尺寸调整为iphone5s的尺寸,也就是4寸屏幕,然后添加两个正方形的view,并排放置在屏幕顶部,一个设置为蓝色,一个设置为粉色。

图片

我们选择iPhone5s模拟器设备并编译运行应用,可以看到一蓝一粉这两个方块按照我们设定的那样杵在竖屏方向的屏幕上,没有问题。当我们把设备调整为横屏时,这两个方块像拥护党一样拥护着他们的坐标位置,所以在横屏的时候看着就不是那么完美:

图片

我们再将模拟器改为iPhone6,然后编译运行,此时在竖屏的时候就已经感觉无法再爱了。这两个方块并没有按照我们设置或设想的那样在屏幕顶部中间,而是偏向了左边,在右边有一块留白区域。

图片

这是就是没有布局约束而导致的,蓝色粉色方块的大小、坐标位置都是固定的,都是在4寸屏幕的参照下设置的,不论在哪种尺寸的屏幕下,它们都在固定的那个坐标位置和固定的大小,所以就会出现上面的情况。

那么接下来让我们给这两个方块添加一些布局约束,再看看会有什么神奇的事情发生。

页边间距约束(Leading and Trailing space)

页边间距约束分前部间距约束(Leading space constaint)和尾部间距约束(Trailing space constaint)。从屏幕上说就是左边距和右边距。我们给蓝色方块添加左边距约束,其值设置为10,给粉色方块设置右边距约束,其值也设置为10。

图片

我们再次在iPhone5s模拟器中运行应用,当横屏时蓝色方块被左边距约束拉到了屏幕左边,粉色方块被右边距约束拉到了屏幕右边。

图片

水平间距约束(Horizontal space constaint)

给蓝色方块和粉色方块添加水平间距约束,其值设置为10。这个约束会使这两个方块之间的距离永远约束为10。

图片

我们再来运行应用,现在横屏时两个方块之间的间距、它们与屏幕边缘的间距都和竖屏时显示的一样了。但是其中粉色方块为了满足水平间距约束自行增加了方块的宽度,变成了长方形。简直是一只老鼠坏了一锅汤有木有。

图片

从上面这个效果我们可以得知,除非我们特别限制了view的尺寸,否则的话iOS会为满足布局约束而改变view的尺寸,也就是保持一个自然的尺寸。

等宽约束(Equal widths constaint)

我们给这两个方块添加一个等宽约束来改善上面的情况。

图片

再次运行应用,现在在横屏时由于等宽约束的作用,两个方块的宽度保持了一致。我们已经非常接近完美了。

图片

方向比例约束(Aspect ratio constaint)

从上面的运行情况来看各个约束都工作正常,但唯一不足的是本来在竖屏是两个正方形方块,在横屏时缺变成了长方形方块,由范爷变成了凤姐这是人类无法接受的。

我们给这两个方块添加方向比例约束来解决这个问题。这个约束使方块在满足其他约束的前提下始终保持高和宽的比例相同。所以在横屏时就会以方块的宽度为比例标准,将高度的比例改为宽度的比例。

图片

用iPhone5s模拟器编译运行应用,现在在横屏状态下两个正方形方块完美的呈现在我们眼前。

图片

我们在iPhone6模拟器上再编译运行应用,从下面的图中我们可以很清晰的看到添加约束之前和之后的变化。布局约束根据多出的空间大小将方块放大到合适的尺寸以满足约束。

图片

实践布局约束

到目前为止,你们已经看到布局约束神奇的作用和效果。是时候让你们在你们自己的storyboard文件中添加布局约束了。

熟练布局约束最好的方法就是多练习,从添加少量的布局约束开始,一步步达到自己想要的效果。

另外有一点,我始终不认为添加过多的布局约束会对应用的性能产生影响。但是我们也不能滥用约束布局,好钢要用在刀刃上,我们尽可能用最节俭的约束布局达到我们想要的效果。

添加布局约束的方式

这里向大家介绍三种在storyboard中添加约束的方式:

1.底部布局约束按钮

这种方式可能是最简单直观的一种方式,在屏幕上选择一个或多个对象,然后点击底部的布局约束按钮添加一个或多个需要的约束。它的好处是可以直观的看到当前选择的对象已经添加了该类中的那些约束。

如果你指选择了一个对象想要添加某个约束,但发现该约束是不可选的,那么就意味着这个约束是适用于两个以上对象的约束。

图片

2.按住Control键拖拽鼠标

你也可以选择一个对象,然后按住Control键和鼠标左键,拖拽鼠标到另一个对象(容器对象,也就是父对象或者选中对象自己),松开鼠标后会弹出适用的约束菜单,你可以选择约束进行添加。

这是我偏爱的一种方式,因为它比上面那种方式来的快多了。

图片

3.菜单/绑定快捷键

你也可以通过菜单选项editor —> pin给一个或多个对象添加布局约束。这是效率最低的一种方式。如果你发现有些约束你会一遍遍的反复添加,那么你就可以给该约束绑定一个快捷键来提高效率。

检查和编辑已添加的布局约束

检查约束最简单的方式就是选中一个对象,然后打开右侧工具栏,选择Size Inspector面板。或者在storyboard界面左侧的结构树中查看约束。

通过这两种方式,你都可以选择某个约束,然后编辑它。

图片

移除布局约束

布局约束可以添加自然也就可以删除。选中某个约束使其高亮显示,然后按下Delete键移除该约束。

如果想移除某个对象上所有的约束,有一个快捷的方式。选择该对象,然后在底部点击Resolve Auto Layout Issues按钮,选择Clear Constaints。

图片

布局出现的问题和冲突

我们在添加约束时,经常伴随有警告或者错误出现。虽然有些警告是因为我们还没有添加完约束出现的,当我们添加完我们设想的约束后警告就会自然消失。但大多数的警告还是指明我们的约束确实存在问题,需要我们修复。

查看警告或错误最方便的方式就是在storyboard左侧的结构树中,在view controller的右边会出现一个红色或黄色的小图标,向我们指明这里存在问题:

图片

错误摆放view的警告

view位置的错误摆放是一个很常见的警告。当一个view没有摆放在约束规定的位置的时候,会出现该警告。

这些问题会在你切换不同设备的模拟器或鼠标不小心移动了某个对象时出现。

选中一个错误摆放的对象,在屏幕上会出现布局约束给你提示的正确的摆放位置。

图片

点击左侧出现的黄色警告图片,会弹出一个菜单,你可以选择让Xcode改变约束,以适应当前该对象的位置,但通常情况下我们都选择让Xcode将该对象移动到约束规定的位置。

缺少布局约束的错误

我们回到早些时候的示例中。这时候我们没有给方块添加Y坐标位置的约束。在运行时没有问题,因为iOS会假设方块的位置就是我们在storyboard中摆放的那个位置。但是Xcode会提示我们一个缺失约束的错误,为了避免意外发生(更换设备尺寸),我还是要根据Xcode的提示添加缺失的约束。

我们给这两个方块添加一个top space to to layout guide约束使方块的位置更加清晰,并消除Xcode的错误提示。

图片

未完待续……

原文地址:ADAPTIVE LAYOUTS FOR iPHONE 6

给TextContainer中添加内容

打开 Main.storyboard ,从组件库(Object Library)中拖拽两个 Label 组件到TextContainer中,位置可以随意摆放:

图片

先选择靠上的Label,然后点击底部的 Align 按钮,添加一个 Horizontal Center in Container 约束,再点击 Pin 按钮,添加一个 Top Spacing to nearest neighbor 约束,设置其值为10:

图片
图片

然后选择右侧工具栏中的 Attribute Inspector 页签,将该Label的 Text 属性设置为 CupertinoColor 属性设置为 WhiteFont 属性设置为 Helvetica Neue, ThinSize 属性设置为 150

这时同学们可能会发现基本看不到刚才设置的文字的全貌,这是因为Label大小的原因。别着急,我们很快就会解决这个问题。

现在选择另一个Label,按照上述的方法给它也添加一个 Horizontal Center in Container 约束以及一个 **Bottom Spacing
to nearest neighbor** 约束,将其值设置为10。打开右侧工具栏中的 Size Inspector 看看:

图片

然后选择 Attribute Inspector 将该Label的 Text 属性设置为 28CColor 属性设置为 White, 将 Font 属性设置为 Helvetica Neue, Thin, 将 Size 属性设置为 250

现在是时候解决Label大小的问题了。选中view controller的view,点击底部的 Resolve Auto Layout Issues 按钮,在弹出菜单中选择 All Views\Update Frames ,现在看看storyboard中发生了什么:

图片

我们看到了刚才设置的城市和温度,但是他们有一点点重叠,这可不是我们想要的结果。在我们修改这个问题之前,先看看预览编辑区的显示情况。我们发现在iPad下显示貌似还挺完美:

图片

但是在iPhone下不出所料的无法直视,字体太大了:

图片

接下来让我们解决这个重叠和字体大小的问题。

Size Classes

通用的storyboard文件固然很好,但是你想真正把它玩转还是得花功夫去研究它,这是一件很有挑战性的工作,当然我们也要懂得运用一些现有的工具来帮助我们。Xcode6就为我们提供了一些工具和技巧,帮助我们更好的实现自适应布局。

自适应布局有一个很重要的概念就是 Size Classes。它并不代表真正的尺寸,而是我们从感官上感觉尺寸的种类,通过这种种类的组合,表示出不同屏幕尺寸设备的横屏及竖屏。

Xcode6为我们提供了两个种类:普通(Regular)和紧凑(Compact)。虽然它们涉及到视图的物理尺寸,但一般它们只代表视图的语义尺寸,即不是真正的尺寸,而是我们从感官上分出的尺寸种类。

下面这个表格向同学们展示了size classes和各个尺寸设备竖屏、横屏之间的关系:

图片

上表中的这些size classes组合都是我们在开发应用中经常碰到的。然而你也可以在视图的任何一个层级中覆盖这些size classes。当以后我们开发Apple Watch应用时显得尤其有用。

Size Classes与开发者

何为设计应用的UI?虽然现在你们的应用已经知道要使用Size Classes,并且你们在storyboard文件中设计应用界面时已经抛开了具体尺寸大小的束缚。但是你们难道没有发现在所有尺寸的设备中,不管是竖屏还是横屏,应用的界面布局都是一致的吗?只是自适应了尺寸大小而已。这还远远不是设计。

当你们决心要设计自适应的界面并已经开始设计的时候,有一点很关键。那就是要知道界面在不同的Size Classes要有继承的关系。你们应该首先设计一个基础的界面,然后根据不同尺寸的横竖屏在基础的界面上进行自定义。千万不要把不同的Size Classes当做独立的屏幕尺寸去设计UI。应该在你们的脑海中建立起界面的一个继承关系的思想,也就是大多数的设备使用基础界面,然后特别的尺寸及横竖屏再根据情况基于基础界面修改。

在本文中,一直没有像大家介绍过如何设置特殊设备的布局,那是应为自适应布局的核心概念Size Classes本身就是由各种特殊设备的特点抽象而来的。也就是说一个Size Classes就意味着一种特殊设备的布局特点,其实普通情况也是特殊情况中的一种。所以说我们可以组合不同的Size Classes来满足各种特殊的布局情况,比如一个支持自适应的视图,它可以在应用的父视图控制器中自适应,也可以在某一个功能的视图控制器容器中自适应。但是两者自适应后的布局却不相同。

这种改进对Apple本身也是有益的,因为他们不断的改变移动设备的尺寸,但从来没有强迫开发者和设计者重新开发和设计他们的应用以适应新尺寸的设备。这就不会让开发者和设计者对Apple不断改变设备尺寸这件事有抗拒心理。

接下来,我们将自定义Size Classes以适应iPhone横屏的时候,因为现在的布局在横屏时用户体验很糟糕。

使用Size Classes

回到 Main.storyboard,点击底部的 w Any h Any,你就可以看到Size Classes的选择器了:

图片

在这个由9个方格组成的网格中,你就可以选择你想在storyboard中显示的Size Class。一共有9种组合方式:3种垂直的也就是竖屏的(任意尺寸(Any),普通(regular),紧凑(compact))选择和3种水平的也就是横屏(任意尺寸(Any),普通(regular),紧凑(compact))的选择。

注意:这里有一点需要大家注意。在Size Classes中,有两个重要的概念叫做水平(Horizontal)垂直(Vertical)。但是在IB中叫做 宽(Width)高(Height)。但他们是等价的,所以大家记住这个概念有两种叫法就可以了。

目前我们的布局在紧凑高度(Compact Height)时显示的很糟糕,也就是iPhone横屏时。我们来解决这个问题,在Size Classes选择器中选择 Any Width | Compact Height 的组合:

图片

这时你会发现在storyboard中会立即出现2个变化:

图片

  1. storyboard中的view controller变成了我们刚才设置的size class。
  2. storyboard底部会出现蓝色的长条区域,并显示出当前我们正在使用的size class。

为了在该size class下改变布局,我们要临时改变一些之前设置好的约束。在自动布局中这种操作有个术语叫做 装配(installing)卸载(uninstalling) 约束。当一个约束在当前的size class中是适用的,我们就将该约束装配在当前的size class中,如果不适用,我们就卸载它。

选择image view,在右侧工具栏中选择 Size Inspector。你可以看到在image view上添加的所有约束:

图片

单击鼠标左键选择 Align Center X to: Superview 约束,然后按下键盘上的 Delete 键来卸载该约束。这时我们可以看到在storyboard中这个约束就立即消失了,并且在storyboard的结构目中和 Size Inspector中该约束都变成了灰色:

图片
图片

注意:你可以在Size Inspector中点击 All 来查看当前size class卸载掉的约束。

鼠标双击刚才卸载的那条约束,我们可以看到在约束编辑界面的底部出现了额外的2个选项:

图片

这两个选项的意思就是这条约束在基础布局中是可用的,但在当前的 Any Width | Compact Height 布局中是不可用的。

按照上面的步骤卸载掉image view上的另外3个约束:

图片
图片

现在你就可以添加适合当前size class的约束了。我们添加一个 Align/Vertical Center in Container 约束,再添加一个 Pin/Left Spacing to nearest neighbor 约束,其值设置为10:

图片
图片

选择image view,按住 Ctrl 键从image view上拖拽至view controller的view上,在弹出的菜单中选择 Equal Widths 约束。

打开右侧工具栏中的 Size Inspector 页签,双击 Equal Width to: Superview 打开该约束的属性编辑界面。如果 First Item 属性的值不是 cloud.Width ,那么点击输入框,在下拉菜单中选择 Reverse First and Second Item。然后将 Multiplier 属性的值设置为 0.45

现在image view在所有的size class中显示应该都没有什么问题了。但是text container view还有点问题。你需要给它添加一个约束,让它显示在该size class屏幕的右侧。

TextContainer view现在有两种约束在身。一种是内部约束,它约束了两个Label的位置,这些约束在各size class中表现的还不错。另一种是外部的约束,它们限制了text container view的左、右、底部与它容器的左、右、底部的间距。这些约束在当前的size class中表现的就不尽如人意了。如果想使text container view在当前size class中位于容器的右下角位置,你得卸载掉左侧的约束。

选中 Left Spacing to nearest neighbor 约束:

图片

Cmd-Delete 卸载该约束,和之前一样,被卸载的约束显示为灰色。

现在你需要再添加两个约束将TextContainer限制在正确的位置上。一个是让text container view的宽度为它容器(view controller的view)宽度的一半。另一个是将text container view固定在顶部。

按理来说,你现在需要选中text container view然后按住Ctrl键和鼠标左键拖动鼠标到view controller view上,然后选择约束。但是目前的情况由于image view和text container view占满了整个view controller,所以你很难选中view controller的view。同学们可以通过storyboard的结构树上进行该操作,会容易很多。

在结构树中选中TextContainer,按住 Ctrl 键和鼠标左键,拖动鼠标到结构树的View上:

图片

弹出菜单中显示了可用的约束,按住 Shift 键点击 Top Space to Top Layout GuideEqual Widths 约束:

图片

然后选中TextContainer,在 Size Inspector 中设置刚刚添加的两个约束:

  • 将 **Top Space to: Top Layout Guide ** 约束的值设置为0。
  • Equal Width 约束的 Multiplier 的值设置为0.5。这里要注意 First ItemSecond Item 这两个属性的值。前者应为TextContainer view,后者为view controller view。如果不一致,那么点击任意一个输入框,选择 Reverse First and Second Item

现在点击storyboard界面底部的 Resolve Auto Layout Issues 按钮,然后选择 All Views\Update frames 。看看发生了什么变化呢:

图片

到目前为止,我们的布局已经越来越接近完美了,唯一一点不足的就是字体大小的自适应,我们会在下一节解决它!

文字属性的自适应

目前TextContainer中的文字尺寸在iPad设备上,也就是使用普通(Regular)size class显示还比较正常。但是当使用紧凑(Compact)size class时文字尺寸就显得太大了,以至于都超出了视图。不过同学们不要怕,我们照样可以在不同的Size Classes中设置不同的文字尺寸来做到自适应。

注意:与重写布局不同,在不同的size class中改变文字的属性始终会影响基础布局中的文字。它不能像布局一样,在不同的size class中设置不同的属性值。我们通过下面的方法来解决这一问题。

回到storyboard文件中,将目前的size class改为最基础的 Any Width | Any Height

选择显示Cupertino的Label,打开 Attribute Inspector 。点击 Font 属性前面的 + 号:

图片

弹出的菜单内容是让我们选择一种size class的组合来重写该组合下的文字属性。我们选择 Compact Width > Any Height

图片

这时就会出现另外一个文字属性下拉框,针对于我们刚才选择的 Compact Width | Any Height size class,我们将字体大小改为90:

图片

再选择显示温度的Label,重复刚才的操作,选择size class组合时选择 Compact Width > Any Height。设置字体大小为150。

在预览区域会自动更新我们刚才的设置:

图片

现在看起来稍微好一些了,但是显示 Cupertino 的Label被截掉了两头。同学们可能会继续调整字体大小使Cupertino显示完全,虽然目前看起来完美了,但是当换一个城市名称后或许又会出现刚才的问题。比如Washington, D.C这么长的名称,又比如Kleinfeltersville, PA这个更长的名称。那么我们应该如何设计呢?

我们的救世主 自动布局(Auto Layout) 再次出马。你只需要给显示城市名称和温度的这两个Label设置一个相对于TextContainer view的宽度约束即可。选中显示Cupertino的Label,按住 Ctrl 键和鼠标左键,拖动鼠标到TextContainer view,在弹出菜单中选择 Equal Widths 约束。对显示温度的Label做相同的操作。之后在预览界面看看发生了什么:

图片

呃……貌似还是有问题,城市名显示不完全。Label中的文字长度超出了允许显示的空间。不过我们可以通过一个选项,让Label自动判断当前的空间可以显示多大的字体。

选择显示Cupertino的Label,然后打开 Attribute Inspector。将 AutoShrink 属性设置为 Minimum font scale,将其值设置为0.5。将 Alignment 属性设置为 Centered

图片

对显示温度的Label做相同的操作。

再来看看预览区域,是不是在不同尺寸的iPhone横屏、竖屏下显示都比较完美了:

图片

是时候在不同的设备上编译运行我们的程序了。用设备来检验才是最保险的。iPhone下的横屏、竖屏是多么的完美:

图片
图片

同学们,至此你们已经学习到了自适应布局最基本的技能。恭喜你们!

原文地址:Beginning Adaptive Layout Tutorial

通用的Storyboard

通用的stroyboard文件是通向自适应布局光明大道的第一步。在一个storyboard文件中适配iPad和iPhone的布局在iOS8中已不再是梦想。我们不必再为不同尺寸的Apple移动设备创建不同的storyboard文件,不用再苦逼的同步若干个storyboard文件中的内容。这真是一件美好的事情。

我们打开Xcode,新建一个项目:

图片

选择iOS\Application\Single View Application创建一个单视图应用:

图片

设置项目名称AdaptiveWeather,语言选择Swift,设备选择Universal

图片

创建好项目后,我们在项目目录结构中可以看到只存在一个storyboard文件:

图片

Main.storyboard文件就是一个通用的storyboard文件,它可以适配目前所有屏幕尺寸的Apple移动设备。打开该文件,同学们会看到一个View Controller,以及一个我们不太熟悉的界面尺寸:

图片

同学们不要吃惊,没错,你们看到的就是一个简单的、有点大的正方形!大伙都知道,在上一个版本的Xcode中,storyboard里的屏幕尺寸都对应着我们所选的目标设备的尺寸,但是这样无法让我们达到“用一个storyboard搞定所有设备”的宏伟目标。所以在iOS8中,Apple将storyboard中屏幕的尺寸进行了抽象处理,也就是说我们看到的这个正方形是一个抽象的屏幕尺寸。

我们接着往下走,选中Main.storyboard文件,然后在右侧工具栏中选择File Inspector页签,然后勾选Use Size Classes选项:

图片

在新的iOS8项目中,该选项默认是勾选的。但当你使用老版本的项目创建新的storyboard文件时就需要你手动进行勾选了。

设置你的Storyboard文件

首先,我们打开Main.storyboard文件,从组件库(Object Library)中选择Image View拖拽到View Controller中。选中刚刚拖入的Image View,在右侧工具栏选择Size Inspector页签,设置X坐标为150,Y坐标为20,为300,为265。

然后再拖入一个View组件,设置X坐标为150,Y坐标为315,为300,为265。

选择你刚才拖入的View,在右侧工具栏中选择Identity Inspector页签,在Document面板中的Label属性输入框中输入TextContainer。这个属性的作用就是给View起一个名字,方便我们辨认。这里要注意一下,Document面板有可能是隐藏的,我们需要点击它后面的 Show按钮来显示它。我们拖入的这个View最后是显示城市和温度Label的容器。

图片

完成上面的设置后,同学们可能会发现刚才拖入的View貌似看不到,这是因为它的背景色和View Controller的背景色是相同的,都是白色,所以我们不太容易辨别。我们来解决这个问题,选中View Controller的View,然后在右侧工具栏中选择Attribute Inspector页签,设置背景色为 红:74,绿:171,蓝:247。然后再选择TextContainer,就是我们拖入的View,设置背景色为 红:55,绿:128,蓝:186。此时Main.storyboard文件中应该是这番景象:

图片

到目前为止,我们在View Controller中添加了两个组件Image View和View,这也是仅有的两个组件,接下来我们就要给它们添加一些布局约束了。

添加布局约束

选择image view,点击底部自动布局工具栏中的Align按钮,勾选Horizontal Center in Container选项,将后面的值设置为0,点击 Add 1 Constraint按钮添加第一个约束。

图片

这个约束的意思是让image view在它的容器(View Controller的View)中保持居中。

然后再点击底部自动布局工具栏中的Pin按钮,添加一个image view顶部与容器顶部间距的约束,我们设置为0:

图片

上面这两个约束使image view处于容器居中的位置,并且它的顶部与容器顶部有一个固定的间距。现在我们需要添加image view和text container view之间的约束。同学们先选中image view,然后按住Ctrl键和鼠标左键,从image view往text container view移动鼠标:

图片

松开鼠标左键后会弹出一个约束菜单,我们选择Vertical Spacing

图片

这个约束决定了image view底部和text container view顶部之间的距离。

现在选中image view然后点击右侧工具栏中的Size Inspector页签,同学们会发现这里在Xcode6中和之前的Xcode版本有所不同:

图片

你会看到之前添加的三个布局约束,你可以在Size Inspector中很方便的修改这些布局约束。比如点击Bottom Space To: TextContainer约束后的 Edit按钮,会弹出约束属性编辑框,我们让Constant的值等于20:

图片

然后点击该弹出框之外的任意地方关闭该弹出框。

你先已经将TextContainer view顶部与image view底部的间距调整到了20,我们还需要添加TextContainer view另外三个边的间距约束。

继续选择TextContainer view,点击底部的Pin按钮弹出 Add New Constraints窗口,在 Spacing to nearest neighbor面板中设置左、右、底部的约束,将值设置为0,然后点击Add 3 Constraints按钮添加约束。这里要注意的是,在设置约束时要将 Constrain to margins选项的勾去掉,这样可以避免TextContainer view产生内边距:

图片

这三个约束会让TextContainer view的左、右、底部三个边与容器的左、右、底部的间距始终为0。

现在Main.storyboard中应该是这番景象:

图片

此时同学们应该会注意到在view上有几个橘黄色的约束线,这意味着还有一些约束上的问题需要我们注意。不过在运行时storyboard会自动更新view的大小来满足它与容器的约束条件。我们也可以点击底部 Resolve Auto Layout Issues 按钮,在弹出框中选择 All Views in View Controller/Update Frames 来修复提示的约束问题,但是如果我们这样做,那么image view的尺寸就会压缩成零,也就是会看不到image view。

这是因为我们的image view还有没有任何内容,但是它有一个缺省的高和宽,并且值为0。进行自动布局的时候,如果被约束的view没有实际的高和宽,那么会依照缺省的高和宽来满足约束条件。

我们接着学习,在项目结构中打开 Images.xcassets ,然后点击左下角的 +号,在弹出菜单中选择 New Image Set

图片

双击左上角的 Image 标题将其改为 cloud

图片

我们刚才新建的这个image set其实就是若干图片文件的一个集合,其中的每一个图片都会对应一个特定的应用场景,也就是针对与不同分辨率的Apple移动设备。比如说,一个图片集合可能会包含针对非视网膜、视网膜、视网膜高清三种分辨率的图片。自从Xcode中的资源库与UIKit完美结合后,在代码中引入图片时我们只需要写图片的名称,程序在运行时会根据当前运行的设备自动选择对应分辨率的图片。

注意:如果你以前使用过通过资源库管理图片,那么你可能会发现在Xcode6中会有所不同。那就是3x图片是怎么回事?

这个新的分片率是专为iPhone 6 Plus提供的。这意味着每一个点是由3个像素点组成,也就是说3x的图片比1x图片的像素多9倍。

目前你的图片集合中还是空的,同学们可以在这里下载需要的图片cloud_images.zip ,然后将图片拖入刚才创建的名为cloud的图片集合中,将 cloud_small.png图片拖到 1x图片区域:

图片

由于我们的图片背景颜色是透明的,所以在图片集合中看到的都是白色的图片。你可以选中某一个图片,然后按下空格键来预览图片。比如选中 1x 图片,按下空格:

图片

现在将 cloud_small@2x.png 图片拖至 2x 图片区域,将 cloud_small@3x.png 图片拖至 3x 图片区域。和之前情况一样,我们看到的只是白色的图片,但我们可以通过空格键来预览图片集合中的图片。

现在你就可以在image view中设置图片了。我们回到 Main.storyboard 中,选中image view,在右侧工具栏中选择 Attribute Inspector 页签,将 Image View 面板中的 Image 属性设置为 cloud,然后将 View 面板中的 Mode 属性设置为 Aspect Fit

图片

现在你的Main.storyboard中应该是这番景象:

图片

我们看到storyboard中一直有橘黄色的约束提示,是时候让我们来修复它们了。首先选中view controller的view:

图片

然后点击底部的 Resolve Auto Layout Issues 按钮,在弹出菜单的 All Views in View Controller 面板中选择 Update Frames

图片

这时,storyboard会自动根据约束条件重新计算view的大小以满足约束:

图片

预览助手编辑器(Preview Assistant Editor)

一般情况下,在这个时候我们应该会在iPad、iPhone4s、iPhone5s、iPhone6、iPhone6 Plus这几个不同尺寸的设备上编译运行程序,以便测试通用的storyboard是否能在不同尺寸的设备上正确的自适应。但这确实是个体力活,一遍一遍的更改设备、编译、运行,多么苦逼。但上天总是会眷顾我们这些苦逼的程序员,Xcode6提供了Preview Assistant Editor,能在一个界面上显示出不同尺寸设备的程序运行情况,是否有问题一目了然。

我们打开 Main.storyboard ,然后选择 View\Assistant Editor\Show Assistant Editor ,这时编辑区会分隔为两部分。再点击顶部导航栏中的 Automatic ,在弹出菜单中选择 Preview ,最后选择 Main.storyboard (Preview)

图片

现在在 Assistant Editor 区域会显示一个4寸的界面:

图片

我们还可以点击预览界面底部,名字(比如图中的iPhone 4-inch)旁边的地方让屏幕翻转为横屏:

图片

这无疑是针对检查不同尺寸设备的自适应情况的一项重大改进,但还远远不止于此!点击预览界面左下角的 + 按钮,会弹出当前storyboard文件支持的各种尺寸的设备,可供我们预览:

图片

分别选择iPhone 5.5-inch和iPad,此时我们在预览界面就可以同时显示三种尺寸的屏幕:

图片

此时同学们是否注意到4寸的横屏显示有点别扭呢?没错,它的那朵元太大了,我们可以通过对image view添加其他的约束条件来改善这个问题。

回到 Main.storyboard ,选择image view,然后按住 Ctrl建和鼠标左键,拖动鼠标到View Controller的View上,松开鼠标后会弹出一个菜单,我们选择 Equal Heights

图片

这时会出现一些红色的约束提示,这是因为我们刚才加的这个约束条件与之前加过的约束条件有冲突。因为之前我们添加过image view和TextContainer view之间的垂直间距(Vertical Margins)约束,所以image view的高度不可能等于它容器(View Controller的View)的高度。

让我们来修复该问题,首先在storyboard的结构目录中选择我们刚才添加的 Equal Heights 约束,然后选择右侧工具栏中的 Attribute Inspect 页签,如果 First Item 属性不是 cloud.Height ,那么在下拉菜单中选择 Reverse First and Second Item 这一项让 First Item 的值成为 cloud.Height

图片

接下来将 Relation 属性的值设置为 Less Than or Equal ,将 Multiplier 的值设置为 0.4

图片

这一系列设置的作用是让cloud这张图片的高度要么等于它自身的高度,要么等于屏幕高度的40%,最后呈现的效果选择这两者中较小的一个高度。

现在你应该注意到了在预览面板中,4寸的横屏显示即时的对你刚才的约束改动做出了响应:

图片

你看看其他尺寸的预览自动更新了么?答案那是必须的,所以说 Preview Assistant Editor 确实是一项重大改进,是程序员和设计人员的福音!

由于本文的示例是一个天气应用,所以光有天气图标不行,我们还得加上城市和温度才行。

未完待续……

原文地址:Beginning Adaptive Layout Tutorial

创建元组

在Swift中创建元组的方式很简单,它的语法有点类似数组,但是需要把方括号替换为圆括号:


let firstHighScore = ("Mary", 9001)

与数组不同的是,元组中的元素可以是任意类型。上面代码中firstHighScore元组就包含一个String类型的元素和一个Int类型的元素。

另外,在创建元组时你还可以给元组中的元素命名:


let secondHighScore = (name: "James", score: 4096)

这样可以让我们在使用元组的时候明确的指定某个元素,非常有用。在后面的文章中大家可以看到给元素命名的好处。

以上就是创建元组的两种方式,非常简单和简洁。你不需要像创建struct一样写出它的结构和内部属性,也不需要像创建class一样要写初始化方法。你只需要把你想用的、任何类型的值放在圆括号内,用逗号隔开即可。如果你愿意你还可以给每个元素命名,提高元组使用效率。

从元组中读元素

从元组中读取元素有几种方式,但一般我们会选择最适合当前应用场景的方式,并且确保选择的方式是在当前情况下最简单的一种。

元组元素没有命名

如果我们没有给元组的元素命名,我们可以用点语法,通过定义好的元组变量或常量获取它的第1个到第n个元素:


let firstHighScore = ("Mary", 9001)
firstHighScore.0 // Mary
firstHighScore.1 // 9001

如果你觉得上述这种方法会造成语义的不明确,那么我们还可以将元组赋值给一个带有元素名称的元组(元素名称个数要对应):


let (firstName, firstScore) = firstHighScore
firstName // Mary
firstScore // 9001

如果你只想读取firstHighScore元组中的分数,那么你可以这样写:


let (_, firstScore) = firstHighScore
firstScore // 9001

元组元素有命名

如果我们已经给元组中的元素命名了名称,那么我们可以这样写:


let secondName = secondHighScore.name
let secondScore = secondHighScore.score
secondName // James
secondScore // 4096

将元组作为函数返回值

我们可以将元组作为函数的返回值,下面这个函数的返回值就是我们之前定义过的secondHighScore元组:


func getAHighScore() -> (name: String, score: Int)
{
let theName = "Patricia"
let theScore = 3894

return (theName, theScore)
}

为什么说上述函数的返回值是secondHighScore元组呢?因为getAHighScore函数返回的元组元素个数、元素名称、元素类型均和secondHighScore相同。

其实将元组作为函数的返回值时也可以不必对元素进行命名,只要你明白每个元素代表的含义即可:


func getAHighScore() -> (String, Int)
{
let theName = "Patricia"
let theScore = 3894

return (theName, theScore)
}

如果你不确定返回的元组一定不为nil,那么你可以返回一个可选的元组类型:


func maybeGetHighScore() -> (String, Int)?
{
return nil
}

因为是可选的元组类型,所以当返回的元组不为nil时,你需要对元组进行解包:


if let possibleScore = maybeGetHighScore()
{
possibleScore.0
possibleScore.1
}
else
{
println("Nothing Here")
}

注意:当你定义了一个没有返回值的函数时,其实该函数是返回一个空的元组()

元组的访问级别

元组的访问级别取决于它包含的元素。比如元组里的元素都是private级别的,那么该元组也是private级别的。但这里有一个遵循最小的原则,也就是说如果一个元组中有两个元素,一个为private级别,另一个为public级别,那么该元组遵循最小原则,它的访问级别为private

元组是值类型

关于值类型和引用类型的知识这里不再累赘,我们通过一个代码示例来看看元组是哪种类型:


var someScore = ("John", 55)

var anotherScore = someScore
anotherScore.0 = "Robert"


println(anotherScore.0) //Outputs: "Robert"
println(someScore.0) //Outputs: "John"

通过上述的代码示例可以看出,我把someScore元组赋值给了anotherScore,然后修改了anotherScore的第1个元素的值,最后分别打印了someScoreanotherScore第1个元素的值。someScore元组第一个元素的值为Robert,而anotherScore元组第一个元素的值仍然为John。由此可见元组是值类型。

我们先想象一下,我们已经有一个很牛逼的应用了,我们姑且叫做重要信息记事本。它的功能就是将我们认为是重要的信息记录下,便于我们随时查看。那么何为重要信息呢?重要信息可以是很多类型的信息,比如武功秘籍、食谱、生日、一段牛逼的代码等等。当我们已经有了这样一个应用后,接下来应该考虑的事情就是如何记录重要信息了。通常思维下,你们可能认为应该是先打开这个应用,然后将重要信息敲进去。是这样么?你当用户是傻子么,什么年代了还手敲,简直弱爆了。下面我向你们介绍一个很酷的方法,那就是通过Action扩展,将信息记录到我们的应用中。下面例子的主要功能是将Safari浏览的网页中的信息保存在我们应用中。

见证奇迹的时刻

先通过File > New > Project菜单创建一个新的工程,选择Single View Application

图片

然后通过File > New > Target菜单给给工程添加一个Target,选择Action Extension

图片

在创建Action扩展时需要指定一个Aciton类型,Apple提供了两种Action扩展的类型模板。一种是有用户界面的类型,包含一个UIViewController和一个Storeboard文件,可以自定义显示界面和行为。另一种是不带用户界面的类型,这种类型只允许我们处理来自Host应用的请求。

图片

现在我们在工程中就可以看到刚才创建的Action扩展NoteAppExtension,它包含两个主要的文件,一个是Action.js,另一个是ActionRequestHandler.swift

图片

我们来看看这两个文件的作用。Action.js文件用来实现和处理浏览器中请求的逻辑,在本文的例子中,它主要实现用户在浏览器中选中文本并发送到我们的应用中。ActionRequestHandler.swift用来处理Host应用发送的请求和参数。

在实现逻辑之前我们需要设置一下扩展的属性,打开Info.plist文件将NSExtensionActivationSupportsWebURLWithMaxCount属性设置为1,该设置让扩展知道我们需要请求一个URL。

图片

我们Action.js文件中有如下内容:


var Action = function() {};

Action.prototype = {

run: function(arguments) {
// 在这个方法里,你可以通过document操作HTML中的元素,或者可以将HTML中的内容传给ActionRequestHandler文件的代码。

// 在本文的例子中,我们不做任何更新,只是将HTML中选中的内容穿给ActionRequestHandler文件的代码。
var selected = "No Text Selected";
if (window.getSelection) {
selected = window.getSelection().getRangeAt(0).toString();
} else {
selected = document.getSelection().getRangeAt(0).toString();
}
arguments.completionFunction({"args" : selected});
},

finalize: function(arguments) {
// 当ActionRequestHandler文件中的itemLoadCompletedWithPreprocessingResults方法执行完之后会调用该方法。

// 如果ActionRequestHandler文件向HTML返回了信息,我们可以通过arguments["message"]来查看,并且可以根据该信息操作HTML中的元素。
alert(arguments["message"])
}
};

var ExtensionPreprocessingJS = new Action

Safari与Action扩展的交互就是通过Action.js文件中的runfinalize这两个方法实现的。当我们在Safari中使用Action扩展时就会调用run方法,它能让我们在该方法中操作当前Safari显示页面的DOM元素。当Action扩展处理完逻辑向Safari返回信息时会调用finalize方法,在我们的例子中,我们通过self.extensionContext!.completeRequestReturningItems(nil, completionHandler: nil)这段代码向Safari返回信息。该方法的第一个参数就是要返回的信息,它会将信息传给Action.js文件,然后通过js代码操作HTML。如果第一个参数传入nil,那就意味着不会调用Action.js文件中的finalize方法。runfinalize这两个方法的参数arguments都包含着一些信息,只不过一个是来自与HTML,一个来自ActionRequestHandler文件。

一定要记住:我们必须要实例化ExtensionPreprocessingJS这个全局变量,因为它是Safari和Action扩展之间的桥梁。

我们的ActionRequestHandler文件内容如下:


class ActionRequestHandler: NSObject, NSExtensionRequestHandling {

var extensionContext: NSExtensionContext?

func beginRequestWithExtensionContext(context: NSExtensionContext!) {
self.extensionContext = context
let identifierType = NSString(format: kUTTypePropertyList, NSUTF8StringEncoding)
for (item: NSExtensionItem) in context.inputItems as [NSExtensionItem] {
for (itemProvider: NSItemProvider) in item.attachments as [NSItemProvider] {
if itemProvider.hasItemConformingToTypeIdentifier(identifierType) {
itemProvider.loadItemForTypeIdentifier(identifierType, options: nil, completionHandler: {(item, error) in
let dictionary = item as NSDictionary
dispatch_async(dispatch_get_main_queue(), {
self.itemLoadCompletedWithPreprocessingResults(dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as NSDictionary)
})
})
}
}
}
}

func itemLoadCompletedWithPreprocessingResults(javaScriptPreprocessingResults: NSDictionary) {
if let text = javaScriptPreprocessingResults["args"] as? String {
let userDefaults = NSUserDefaults(suiteName: "group.name")
userDefaults.setValue(text, forKey: "note")
userDefaults.synchronize()
self.doneWithResults(["message": "Successfully added to the note app"])
}
}

func doneWithResults(resultsForJavaScriptFinalizeArg: NSDictionary?) {
if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg {
let identifierType = NSString(format: kUTTypePropertyList, NSUTF8StringEncoding)
// 创建合适返回类型的标示符。

// 这里创建的resultsItem将作为Action.js文件中finalize方法的参数。
var resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize]

var resultsProvider = NSItemProvider(item: resultsDictionary, typeIdentifier: identifierType)

var resultsItem = NSExtensionItem()
resultsItem.attachments = [resultsProvider]

// 这段代码意味着Action扩展已经处理完了逻辑,现在将信息返回给Action.js文件。
self.extensionContext!.completeRequestReturningItems([resultsItem], completionHandler: nil)
} else {
// 就算我们没有任何要返回的信息,也要执行这段代码,用于告知我们的Action扩展已经完成了逻辑处理。
self.extensionContext!.completeRequestReturningItems(nil, completionHandler: nil)
}

self.extensionContext = nil
}
}

现在我们可以运行一下我们的应用,然后打开Safari,在Action选项中开启我们的Action扩展:

图片

然后我们就可以在Action栏中看到我们的扩展了:

图片

我们使用Safari随便打开一个含有文字的页面,选中一段文字,然后打开Action栏,点击NoteApp扩展,此时我们的扩展就会将选中的这段文字发送给我们的应用,形成一条新的重要信息。

图片
图片
图片

以上只是一个简单的Aciton扩展的例子,但我们可以由此延伸出更多有用、有创意的功能,让我们的生活更加美好。

本文例子的代码可以在这里下载:NoteApp

原文地址:Action Extension in Swift