1、基本概念
在iPad和iPhone 5出现之前,iOS设备就只有一种尺寸。我们在做屏幕适配时需要考虑的仅仅有设备方向而已。而很多应用并不支持转向,这样的话就完全没有屏幕适配的工作了。随着iPad和iPhone 5,以及接下来的iPhone 6的推出,屏幕尺寸也变成了需要考虑的对象。在iOS7之前,为一个应用,特别是universal的应用制作UI时,我们总会首先想我们的目标设备的长宽各是多少,方向变换以后布局又应该怎么改变,然后进行布局。iOS6引入了AutoLayout来帮助开发者使用约束进行布局,这使得在某些情况下我们不再需要考虑尺寸,而可以专注于使用约束来规定位置。
既然我们有了AutoLayout,那么其实通过约束来指定视图的位置和尺寸是没有什么问题的了,从这个方面来说,屏幕的具体的尺寸和方向已经不那么重要了。但是实战中这还不够,AutoLayout正如其名,只是一个根据约束来进行布局的方案,而在对应不同设备的具体情况下的体验上还有欠缺。一个最明显的问题是它不能根据设备类型来确定不同的交互体验。很多时候你还是需要判断设备到底是iPhone还是iPad,以及现在的设备方向究竟是竖直还是水平来做出判断。这样的话我们还是难以彻底摆脱对于设备的判断和依赖,而之后如果有新的尺寸和设备出现的话,这种依赖关系显然显得十分脆弱的(想想要是有iWatch的话..)。
所以在iOS8里,Apple从最初的设计哲学上将原来的方式推翻了,并引入了一整套新的理念,来适应设备不断的发展。这就是SizeClasses。
不再根据设备屏幕的具体尺寸来进行区分,而是通过它们的感官表现,将其分为普通(Regular)和紧密(Compact)两个种类(class)。开发者便可以无视具体的尺寸,而是对这这两类和它们的组合进行适配。这样不论在设计时还是代码上,我们都可以不再受限于具体的尺寸,而是变成遵循尺寸的视觉感官来进行适配。
SizeClasses有三个值:Regular,Compact和Any。Any是什么意思呢?如果weight设为Any,height设置为Regular,那么在该状态下的界面元素在只要height为Regular,无论weight是Regular还是Compact的状态中都会存在。这种关系应该叫做继承关系,具体的四种界面描述与可继承的界面描述如下:
1 2 3 4 |
|
这么多设备(iPhone 4S,iPhone 5/5s,iPhone 6,iPhone 6Plus,iPad,AppleWatch)的尺寸,就通过SizeClasses简单的表达出来了:
iPhone4S,iPhone 5/5s,iPhone 6
竖屏:(w:Compacth:Regular)
横屏:(w:Compacth:Compact)
iPhone6Plus
竖屏:(w:Compacth:Regular)
横屏:(w:Regularh:Compact)
iPad
竖屏:(w:Regularh:Regular)
横屏:(w:Regularh:Regular)
AppleWatch(猜测)
竖屏:(w:Compacth:Compact)
横屏:(w:Compacth:Compact)
PS:附上图形:
2、UITraitCollection和UITraitEnvironment(Size Classes手写代码)
为了表征SizeClasses,Apple在iOS 8中引入了一个新的类,UITraitCollection。这个类封装了像水平和竖直方向的SizeClass等信息。iOS 8的UIKit中大多数UI的基础类(包括UIScreen,UIWindow,UIViewController和UIView)都实现了UITraitEnvironment这个接口,通过其中的traitCollection这个属性,我们可以拿到对应的UITraitCollection对象,从而得知当前的SizeClass,并进一步确定界面的布局。
和UIKit中的响应者链正好相反,traitCollection将会在viewhierarchy中自上而下地进行传递。对于没有指定traitCollection的UI部件,将使用其父节点的traitCollection。这在布局包含childViewController的界面的时候会相当有用。在UITraitEnvironment这个接口中另一个非常有用的是-traitCollectionDidChange:。在traitCollection发生变化时,这个方法将被调用。在实际操作时,我们往往会在ViewController中重写-traitCollectionDidChange:或者-willTransitionToTraitCollection:withTransitionCoordinator:方法(对于ViewController来说的话,后者也许是更好的选择,因为提供了转场上下文方便进行动画;但是对于普通的View来说就只有前面一个方法了),然后在其中对当前的traitCollection进行判断,并进行重新布局以及动画。代码看起来大概会是这个样子:
override func willTransitionToTraitCollection(newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator){ super.willTransitionToTraitCollection(newCollection, withTransitionCoordinator: coordinator) coordinator.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext!) -> Void in if (newCollection.verticalSizeClass == UIUserInterfaceSizeClass.Compact) { //To Do: modify something for compact vertical size } else { //To Do: modify something for other vertical size } self.view.setNeedsLayout() }, completion: nil) }
在两个To Do中,我们应该删除或者添加或者更改不同条件下的AutoLayout约束(当然,你也可以干其他任何你想做的事情),然后调用-setNeedsLayout来在上下文中触发转移动画。如果你坚持用代码来处理的话,可能需要面临对于不同SizeClasses来做移除旧的约束和添加新的约束这样的事情,可以说是很麻烦(至少我觉得是麻烦的要死)。但是如果我们使用IB的话,这些事情和代码都可以省掉,我们可以非常方便地在IB中指定各种SizeClasses的约束(稍后会介绍如何使用IB来对应SizeClasses)。另外使用IB不仅可以节约成百上千行的布局代码,更可以从新的Xcode和IB中得到很多设计时就可以实时监视,查看并且调试的特性。可以说手写UI和使用IB设计的时间消耗和成本差距被进一步拉大,并且出现了很多手写UI无法实现,但是IB可以不假思索地完成的任务。从这个意义上来说,新的IB和SizeClasses系统可以说无情地给手写代码判了个死缓。
另外,新的API和体系的引入也同时给很多我们熟悉的UIViewController的有关旋转的老朋友判了死刑,比如下面这些API都弃用了:
@property(nonatomic, readonly) UIInterfaceOrientation interfaceOrientation - willRotateToInterfaceOrientation:duration: - willAnimateRotationToInterfaceOrientation:duration: - didRotateFromInterfaceOrientation: - shouldAutomaticallyForwardRotationMethods
现在全部统一到了viewWillTransitionToSize:withTransitionCoordinator:,旋转的概念不再被提倡使用。其实仔细想想,所谓旋转,不过就是一种Size的改变而已,我们都被Apple骗了好多年,不是么?
3、InterfaceBuilder中使用SizeClasses
创建一个新的通用项目。如果你想要早在一个已经创建了的Xcode6项目,你需要激活sizeclasses选项。你可以在InterfaceBuilder中的属性面板勾选autolayout的选项的下面找到它。
首先,让我们在Xcode中看一下sizeclass的网格。这是一个你可以在不同的布局排列间切换的区域。当你查看storyboard的时候,看到视图的底部,并且点击‘wAnyhAny’字样的标签。你将会看到一些类似网格的画面。
默认的,我们以一个基础的设置开始,也就是anywidth和anyheight。很多事情都将在这里安置和改变,包括了iphone和ipad的所有方向的默认布局。苹果建议把大多数的设置都在这个界面中进行设置。这个是因为减少工作量而显得特别的简单。让我们布局一个超级宽的按钮在画面的中间。给它一个绿色的背景,从而让我们看到它真实的尺寸,给它一个约束来让他居中。
并且给它一个夸张的固定宽度600。
好了,现在在ipad和iphone的模拟器都运行一下,你将会看到都是居中,但对于iphone的两个方向都太宽了,(这里你设置了页面中button的宽度但并没有马上更新是因为你在做添加约束的时候没有更新图形,导致了如下图的情况,storyboard里面没有更新,而在模拟器运行时候更新了,左边大纲栏目里面也有警告说明,可以直接点击警告里面的黄色三角来更新画面其实就是UpdataFrame)
让我们使用sizeclasses来修正吧。回到刚才那个第一张图的网格选择iphone的纵向(portrait)设置,就是紧凑的宽度+常规的高度。网格中的红色矩形.
你将会注意到你在网格中选中之后底部的bar改变为蓝色。那是在警告你:“Hey,你并不是在一个基础的设置,有些改变将会只在你运行的时候显示。所以这个bar现在是蓝色的!”我所说的一些改变是因为有四项你能改变的sizeclasses:1约束常数,2字体,3约束的开/关,4子视图的开/关。
前两个是不言而喻的,但是让我来告诉你如何让后两者工作。在当前的sizeclass(compactwidth和regularheight)状况下让我们试着把一个约束关闭。在文档的提纲栏里,点击设置在我们的button的CentreX校准约束:
在看一下我们的属性检查栏,在底部我们可以看到带标记的一个单词“Installed”,并且左侧有额外的加号按钮。点击额外的加号并且点选‘CompactWidth|RegularHeight‘(当前的就是)。
现在你将会看到2个标记物,把刚刚添加的哪一个取消勾选(wChR)
现在我们的约束不再安置并且做任何事情来配置sizeclasses。就像你看到的,Xcode正在控诉我们的约束太混乱了(左边的大纲会有错误提示表示你缺少了约束-译者),如果你这时候运行app在iphone的模拟器上的话,按钮不在X方向居中了。但是在ipad的上面还是居中的,因为约束仍然安置在基本的设置里面。这个约束将会一直配置着除非我们把它取消勾选。你甚至能够旋转你的iphone模拟器,并且发现button将会神奇的回到居中,因为iphone的横向是不同的sizeclass配置,好了,让我们把勾选回来,让button回到居中。
现在让我们改变我们设置在button宽度的约束,选择button,并且来到Size的属性检查栏,下拉到底部,我们可以看到所有的约束。点击Width原本是600的使用Edit设置为100:
在iPhone的模拟器上运行,你将会看到button已经具备了正确的宽度。运行在ipad的模拟器的时候却展示了600的宽度,因为我们没有改变基本设置里面的宽度。但是,在iphone的横向landscape仍然看着不怎么样,因为iphone的横向设置来自基本的AnyAny的设置。让我们修正一下。在网格里面我们选择compactWidth和CompactHeight。也就是第一张图的蓝色网格。
现在我们在这个设置下改变width的约束,就像我们为了compactxregular改变的一样。给予一个400的宽度。运行一下iphone的模拟器,并且旋转到横向,按钮有了400的宽度,看上去很棒。达到了我们的预想。有一点很好就是你能看到一个所有的约束的列表,这些都是不同的设置的。仅仅选择你想要在文档大纲里面看到的约束,然后来到属性检查栏,他们整齐的排列在初始的常数下面。它标注了每一个基于它所应用的设置。
即使我们决定我们想要只在iphone横向landscape模式下button消失,使用sizeclasses我们只要反向安置views就像我们反向安置一个约束。选择我们的UIbutton,滚动到属性检查器的底部。通过点击加号按钮给我们当前的设置添加一个新的安置选项,然后取消勾选它。
就像你看到的,那个view立马消失了,因为我们在设置里面反向安置了它,我们立马就能看到。运行app,你能看到它在纵向的portraitiphone上消失了,但是当你旋转到横向的landscape的时候又回来了。当然它也一直安置在ipad上面因为ipad仍然使用的是基本的设置。
4、SizeClasses和ImageAsset及UIAppearence
ImageAsset里也加入了对SizeClasses的支持,也就是说,我们可以对不同的SizeClass指定不同的图片了。在ImageAsset的编辑面板中选择某张图片,Inspector里现在多了一个Width和Height的组合,添加我们需要对应的SizeClass,然后把合适的图拖上去,这样在运行时SDK就将从中挑选对应的Size的图进行替换了。不仅如此,在IB中我们也可以选择对应的size来直接在编辑时查看变化。
实际做起来实在是太简单了..但拿个demo说明一下吧,比如下面这个实现了竖直方向Compact的时候将笑脸换成哭脸--当然了,一行代码都不需要
另外,在iOS7中UIImage添加了一个renderingMode属性。我们可以使用imageWithRenderingMode:并传入一个合适的UIImageRenderingMode来指定这个image要不要以Template的方式进行渲染。在新的Xcode中,我们可以直接在ImageAsset里的RenderAs选项来指定是不是需要作为template使用。而相应的,在UIApperance中,Apple也为我们对于SizeClasses添加了相应的方法。使用+appearanceForTraitCollection:方法,我们就可以针对不同trait下的应用的apperance进行很简单的设定。比如在上面的例子中,我们想让笑脸是绿色,而哭脸是红色的话,不要太简单。首先在ImageAsset里的渲染选项设置为TemplateImage,然后直接在AppDelegate里加上这样两行:
UIView.appearanceForTraitCollection(UITraitCollection(verticalSizeClass:.Compact)).tintColor=UIColor.redColor() UIView.appearanceForTraitCollection(UITraitCollection(verticalSizeClass:.Regular)).tintColor=UIColor.greenColor()