苹果刚发布了iOS9,在iPad上新增了两个新的特性SlideView和SplitView,前者可以在不关闭当前激活APP的情况下调出来另外个APP以30%比例显示进行操作使用,后者允许同时运行两个APP以50%50%,70%30%比例运行,感觉非常方便。
然而,方便了用户的同时却恶心了开发者,在同一屏幕运行两种APP的时候势必APP显示比例发生改变,那么就需要对几种不同的大小进行处理,好在苹果有Autolayout,并且在iOS8中新增了SizeClass特性,两者结合,可以很好的应付以上各种情况。
好了,为了适配iOS9上述的特性,先来看下苹果的文档说明来如何处理多种显示比例的问题。Adopting Multitasking Enhancements on iPad中对这种情况作了很好的描述,最主要的就是先理解一幅图片。
在iOS8中新增的SizeClass能很好理解图片的内容,C(Compact)紧凑,R(Regular)常规,通过C和R的组合可以匹配出各种屏幕,如果不理解最直观的可以查看StoryBoard中设置Autolayout时候在底部出现的w Any h Any通过鼠标移动可以得出各种组合后能适配哪一种屏幕,这里就不再阐述。
此图可见,在iPad标准的屏幕比例种,width和height都是R,也就是说无论横屏还是竖屏都是常规的组合即wR hR,然而在出现split和slide后状态即发生的改变,在竖屏状态下APP被分割后出现了wC hR,在横屏状态下,又出现了两种组合分别是主APP70%从APP30%和主APP50%从APP50%,通过图片得出在横屏7:3中,主APP比例是wRhR,从APP比例是wChR,在5:5情况下主从APP都是wChR,那么就此知道了APP在slideView和splitView状态下的各种高宽组合。
总结一下APP在Slide和Split后的各种需要适配的尺寸是,100%常规状态,70%作为主APP的状态,50%作为split等分的状态,30%作为从APP出现时候的状态。由于100%和70%都属于wRhR,那么我们主要适配就分成三中情况 100%,50%,30%,如果APP界面主要以list为主或者比较简单的布局,其实只要适当调整Autolayout的offset值即可适配所有的情况,那么如果是比较复杂的界面或者需要满足各种状态下的显示怎么办呢,当然是有解决的方案,以下主要以简单代码的例子进行适配工作,主要理解原理和知道什么时候触发显示比例改变,还有种方法是通过Storyboard的SizeClass匹配上述所有状况并且逐一调整差值,这种方式比较简单用惯XIB的应该很容易解决,缺点就是维护起来稍微不方便。
首先,有个需求,在屏幕当中放置一个红色的UIView,在正常状态下,左右两边距边框100个像素,并且有个label显示当前的比例,当出发split或者slide的时候,UIView的左右边框调整为10个像素。具体的结果如下图:
上图为正常的全屏,下图为splitView之后的。
首先代码先在view内增加一个红色的UIView和一个label用于显示当前状态。
1 var testingView:UIView! 2 var collectionStateLabel:UILabel! 3 4 testingView = UIView() 5 testingView.backgroundColor = UIColor.redColor() 6 self.view.addSubview(testingView) 7 8 collectionStateLabel = UILabel() 9 collectionStateLabel.textAlignment = NSTextAlignment.Center 10 collectionStateLabel.textColor = UIColor.blackColor() 11 self.view.addSubview(collectionStateLabel)
接着,开始分析实际情况,通过模拟器或者真机使用后就会发现,在应用程序启动的时候就可能出现好几种情况,红色数字代表我的APP显示比例
场景1 如果我正在浏览照片,这时候突然想打开APP查看某样东西的时候那么这时候就会发生几种情况。
1 程序以SlideView启动。 (10:3)
2 在通过SlideView启动后又展开到了SplitView。 (5:5)
3 在SplitView使用后我觉得不爽,太小了,再想进一步展开又成为全屏。 (0:10)
场景2 如果我正在使用APP,这时候我想通过地图查看某幢楼在哪里,这时候又会发生几种情况。
1 程序正在使用时,接受地图程序以slide方式切入,此时地图程序被激活,可以查询地图 (10:3)
2 在查询地图的时候我还要使用回我的APP,这时候我的APP被激活,地图同样被激活 (7:3)
3 我想放大地图程序被地图以SplitView切割成一半显示。 (5:5)
从上总结出来,场景1我的APP是作为从APP存在,照片是主APP,场景2我的APP是作为主APP存在,地图程序是从APP,其实这都不重要,最主要的得出结论就是在布局的时候一开始就必须考虑到针对不同的场景以适应不同的布局需求。
现在开始代码布局,代码布局使用了一个第三方的类库以节省代码量,Apple的API实在非常的繁琐,在此使用SnapKit作为布局类库 (https://github.com/SnapKit/SnapKit),OC版本(https://github.com/SnapKit/Masonry)
从场景1,2分析出在一开始就需要知道当前的屏幕处在什么样的比例之中,那么通过文章一开始分析的Apple文档得出在slide和split下的比例都是wC hR也就是说宽是紧凑竖是标准。那么通过SizeClass的API就可以判断出来。
if self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClass.Regular && self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact{ //slide or split size slide和split状态 }else{ //regular size 标准状态 }
通过 UITraitCollection 类可以获取当前屏幕处在什么样子的比例当中 ,这个类封装了各种水平竖直方向等SizeClass的信息,通过实现了UITraitEnvironment接口的对象都可以拿到这个属性,(UIViewController,UIView,UIWindow,UIScreen都实现了这个接口)。通过判断属性verticalSizeClass和horizontalSizeClass的各种组合即可很容易获取到当前屏幕水平垂直配比。
至此,屏幕显示比例判断出来了,那么就根据需求和实际情况分别编写不同比例下的适配代码即可,这里根据需求在regular下按钮左右边距100像素,在split下按钮左右10个像素。
func setViewToRegularSize(){ collectionStateLabel.text = "State:Regular View" guard testingView.constraints.isEmpty else{ testingView.snp_updateConstraints { (make) -> Void in make.left.equalTo(self.view.snp_left).offset(100) make.right.equalTo(self.view.snp_right).offset(-100) make.centerY.equalTo(self.view.snp_centerY) } return } testingView.snp_makeConstraints { (make) -> Void in make.left.equalTo(self.view.snp_left).offset(100) make.right.equalTo(self.view.snp_right).offset(-100) make.centerY.equalTo(self.view.snp_centerY) make.height.equalTo(60) } } func setViewToSlideSplitSize(){ collectionStateLabel.text = "State:SplitView or SlideView" guard testingView.constraints.isEmpty else{ testingView.snp_updateConstraints(closure: { (make) -> Void in make.left.equalTo(self.view.snp_left).offset(10) make.right.equalTo(self.view.snp_right).offset(-10) make.centerY.equalTo(self.view.snp_centerY) }) return } testingView.snp_makeConstraints { (make) -> Void in make.left.equalTo(self.view.snp_left).offset(10) make.right.equalTo(self.view.snp_right).offset(-10) make.centerY.equalTo(self.view.snp_centerY) make.height.equalTo(60) } }
我们设置了两个函数第一个函数适配regular的情况,第二个函数适配Split或Slide的情况,并且分别在label上标注,这里使用了swift guard ... else {}来判断如果约束不为空则更新约束,否则新增约束,关于snapkit用法具体请看GIT上说明,这里仅仅举例。现在分别在初始化时候调用相应的函数即可完成APP启动时候的显示适配。并且给label上好约束。
if traitCollection.verticalSizeClass == UIUserInterfaceSizeClass.Regular && self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact{ //slide or split size self.setViewToSlideSplitSize() }else{ //regular size self.setViewToRegularSize() } collectionStateLabel.snp_makeConstraints { (make) -> Void in make.top.equalTo(testingView.snp_bottom).offset(50) make.centerX.equalTo(testingView.snp_centerX) }
到了这一步已经满足了,APP从slide进来时候的适配。但是,事情还没那么简单,通过场景1,2得出,在不满足当前大小的情况下可以从slide过渡到split,甚至从split过度到regular,那么就牵涉到动态更改布局了,好在Apple的API提供了一个函数来的到当前sizeClass的改变。在viewcontroller中输入以下代码
override func willTransitionToTraitCollection(newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) { super.willTransitionToTraitCollection(newCollection, withTransitionCoordinator: coordinator) if newCollection.verticalSizeClass == UIUserInterfaceSizeClass.Regular && newCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact{ self.setViewToSlideSplitSize() }else{ self.setViewToRegularSize() } }
这个函数类似于以前willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval)处理旋转屏幕的逻辑一样,当sizeClass发生改变后立即会得到调用,那么在这个函数内根据Apple文档提供的slide和split的比例规则也非常容易对当前布局进行更新。
至此,我们已经完全满足了需求和场景1和2的各种情况,综上所述,只要知道sizeClass的各种比例组合就可以轻松应付各种屏幕显示发生改变的情况,再通过与Autolayout的配合,达到满足各种尺寸及动态改变尺寸需求。最后,虽然苹果引入的新的特性,看似复杂,其实还是使用老的技术来解决各种情况,sizeClass和autolayout配合犹如双剑合壁,无惧任何尺寸大小的变更。
题外话,如果习惯使用XIB的话用法还是和之前的一样,只需要匹配各种Compact和Regular的组合并且设置好相应的约束并且Install对应的View也非常容易对付Slide和Split
只需要动动鼠标修改下约束也很好的满足实际的情况,只是storyboard的维护性和可读性不是很友好,代码可能会更容易做出修改维护和抽象,实际应用我代码使用的比较多点。