翻译:Building an Adaptive Interface-翻译-size class和present view controller

96
金风细细
2016.09.29 15:02* 字数 5644

写在之前的大篇废话

研究adaptivePresentationStyleForPresentationController和iPhone上popover一个popoverPresentationController的关系无果后,一怒之下翻译该文档,

希望不要浪费太多时间 -- 9月28号

结果学到了很多东西,虽然还没解决问题 -- 9月29号

问题解决了,原来都在文档里,只是我没读懂读透 --9月30号


废话:-----通过这次翻译的经历,感觉到沉下心来做一件事的重要性,28号的时候无意间研究到iphone的popover,发现自己怎么也不能在iphone上做出一个popover出来,老是全屏,捣鼓了大半天,把度娘和谷歌翻了个遍,觉得确实是照着人家的帖子做的,为啥不出呢?

翻到ios官网的这篇文章,总觉得读了没有收获,(我英语都认识,可是读完也不知道它在说什么)但是又觉得它确实提到了我所说的问题,反正也挺无奈的,就决定逐字逐句的读一读这文章,看看是不是自己有什么没懂的地方.

每句读了也不了解的话,我都姑且照字翻译了下,然后会去查阅相关的文档,官网优先,百度也问,也翻这本书:Programming.iOS.9.在逐步学习的过程中,发现了很多平时我掌握的似是而非的东西,如size classes,如vc间的present dismiss等等,虽然做ios已经两年了,这些东西每天都在弄,但是它很多细微的东西还没有深入的了解.

慢慢的融会贯通几个知识点,理解了很多东西,虽然这些东西平时没人会问你为什么,找工作别人也不会问这么细,浪费时间想这些似乎没有多大的意思,没有弄些时尚流行的技术来得可以吹niub.但是还是感触很深,很多简单的功能,会做.但是为什么这么做,深层的意义在哪里,内涵思想是什么,研究了这些细节后,才有所体会.之前还是很心浮气躁,比如9月28号的时候,看了五六个帖子,觉得自己已经会了popover的步骤,就是不出效果,快要放弃了.这两天看了一些文档,才领会了为啥这么做,为啥要写代理,还可以举一反三等等.读的多了,发现某些大神的所谓深入帖子,也只是把官网的几篇文档综合了一下,加上自己的总结.可见编程其实和做学问差不多,要放下心中的浮躁,慢节奏的读一些似乎没用的东西.

问题的解决经过:

之前照着这个帖子做的,http://www.jianshu.com/p/e44542c38fc9 始终不能出现popover,总是全屏,我认为肯定是adaptivePresentationStyleForPresentationController这个函数出的问题,调试发现从来没进入过,然后差点放弃了.

我也没问过自己为什么一定要实现这个方法,原因是啥.就知道很多帖子都说了这个, 直到翻译完了文档,才知道:

其实在swift3中不是这个函数(OC中是上面那个函数),而是:

func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle{

return .none

}

1) 它是UIViewController的属性presentationController的代理UIPresentationControllerDelegate的函数.

2) 同理,UIViewController的属性popoverPresentationController的代理UIPopoverPresentationControllerDelegate,由于继承了UIPresentationControllerDelegate的原因,所以也要实现这个方法.

关系如图1-1-1:

1-1-1

实现了它顿时就好了.所以坑就在这里

这是我的代码:


@IBAction func clickBtn(_ sender: AnyObject) {

let pop = PopViewController()

//这个属性必须实现,否则popoverPresentationController为nil

pop.modalPresentationStyle = .popover

//设置代理,用 adaptivePresentationStyle(for controller) 指定不要全屏显示

pop.popoverPresentationController?.delegate = self

//popover 的大小

pop.preferredContentSize = CGSize(width:100, height:100)

//popover基于哪个view出来

pop.popoverPresentationController?.sourceView = btn

pop.popoverPresentationController?.sourceRect  = CGRect.zero

//        pop.popoverPresentationController?.sourceRect  = CGRect(origin:CGPoint(x:100,y:100) ,size:CGSize(width:0,height:0))

//popover的箭头方向

pop.popoverPresentationController?.permittedArrowDirections = .down

self.present(pop, animated: true, completion: nil)

}

xib或者storyboard里的pop 不需要做任何改动.比如这个帖子写的https://my.oschina.net/sayonala/blog/533888  都不用

效果:

1-1-2


翻译之前的储备


1. size classes

1) size classes 是ios8引进的的一种对布局的自适应的解决方案.可以从以下两个方面理解:

对不同设备的屏幕尺寸,包括横屏和竖屏,我们可以设定不同的view布局,系统会检测到具体的设备,帮我们自动切换对应的布局.

对同一套布局,系统会检测到具体的设备和屏幕方向,遵循autoulayout的设定,自动布局.

于是我们可以无视具体设备的尺寸和屏幕方向,根据size classes 的规定来布局了.

2)  在ios8后,苹果定义所有设备的size class只分为两种属性: 详见枚举UIUserInterfaceSizeClass

. 普通的(Regular)

. 紧凑的(Compact)

Any包含了上面两种情况. 所以nib中的width:Any和height:Any代表:包含了所有情况.

如果在ios8后看到这种判断设备和尺寸的代码:就不太好(ios7时我刚入门ios,看到我们公司项目中这么写的,当时没有size classes)

//iphone4 and iphone4s

if (UIScreen.main.bounds.height == 480) {

}

//iphone5 and iphone5s

else if (UIScreen.main.bounds.height == 568) {

}

那么具体设备尺寸和 Compact,Regular的对应关系是?

这张图1-1来自于UITraitCollection的api文档 https://developer.apple.com/reference/uikit/uitraitcollection


1-1

总结下:

1) ipad不管横竖宽高size class都是regular, 

2) iPhone横着时宽高size class都是compact,竖起来高会变成regular.

3)iPhone 6plus特殊点,不管横竖 长边就是regular,短边是compact  ... iphone7不画,因为它尺寸和iphone6s没有变化.


现在ios10 nib编程已经不用背这个了,为啥,见图13-1. 它把尺寸和设备都形象化了

如果用代码,我们还是要了解对应关系的,不然我们获取出来了屏幕width和height的size class,还不知道当前设备是个啥情况呢.


2. trait - UITraitCollection和其接口UITraitEnvironment

上面说取屏幕width和height的size class,如何取?取出来后,我们可以根据不同的尺寸和横竖屏,更改布局,如隐藏或者显示某些view等.

trait英文是特性的意思,我理解就是UITraitCollection.也就是关于尺寸,设备屏幕的一系列属性的大集合.

https://developer.apple.com/reference/uikit/uitraitcollection 这是 UITraitCollection的官方文档

UITraitCollection是接口UITraitEnvironment的属性.它包含了设备关于屏幕的众多trait,size class也是其中一个属性:

1) 水平和竖直上的size class,horizontalSizeClass,verticalSizeClass

2) 显示缩放比 displayScale

3) 用户设备的分类 userInterfaceIdiom(枚举UIUserInterfaceIdiom).

public enum UIUserInterfaceIdiom : Int {

case unspecified  //未定义

case phone // iPhone and iPod touch style UI

case pad // iPad style UI

case tv // Apple TV style UI  apple做电视和车载,我还没看到大陆哪里有买哇-_-

case carPlay // CarPlay style UI

}

UIScreen, UIWindow, UIViewController, UIPresentationController,UIView都实现了UITraitEnvironment接口,因此可以很方便的直接取用:

self.traitCollection

两个函数,在trait发生改变时被调用:

1.public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)  

它是接口UITraitEnvironment的方法.上面提到的UIScreen, UIWindow, UIViewController, UIPresentationController,UIView都可以实现它.当trait发生改变时,进入这个方法.我们通过判断其属性改变布局:

代码:

//MARK: UITraitEnvironment

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)  {

//iphone 横屏

if previousTraitCollection?.horizontalSizeClass == .compact &&  previousTraitCollection?.verticalSizeClass == .compact{

print("iPhone从横屏->竖屏")

}

else if (previousTraitCollection?.horizontalSizeClass == .compact &&  previousTraitCollection?.verticalSizeClass == .regular){

print("iPhone从竖屏->横屏")

}

}

注意:1) previousTraitCollection是之前的设备状态,不是当前的.而且有可能是nil,比如程序最开始启动的时候还没有之前状态哟

        2) 横竖屏切换记得先打开手机设置中的横竖屏开关


2.public func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)

这个方法是接口UIContentContainer的方法,所幸UIViewControllerUIPresentationController都实现了它. 它也是在trait改变时进入的.它先于traitCollectionDidChange被调用.

可以在里面写动画coordinator.animateAlongsideTransition ...

要调用super.willTransition(to: newCollection, with: coordinator).  除非自己实现子viewcontroller的变化

代码:

//MARK: UIContentContainer

override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator){

super.willTransition(to: newCollection, with: coordinator)

coordinator.animateAlongsideTransition(in: self.view, animation: { (context) in

//动画,随便写点啥把.

self.btn.titleLabel?.text = "gugu"

}) { (context) in

//iphone 横屏

if newCollection.horizontalSizeClass == .compact &&  newCollection.verticalSizeClass == .compact{

print("iphone目前是横屏")

}

else if (newCollection.horizontalSizeClass == .compact &&  newCollection.verticalSizeClass == .regular){

print("iphone目前是竖屏")

}

}

}


3. 3D touch和trait的关系

啰嗦一点,ios9中,在ViewController中判断设备是否支持3Dtouch.就是这么写的:

if(self.traitCollection.forceTouchCapability ==UIForceTouchCapabilityAvailable){

[self registerForPreviewingWithDelegate:self sourceView:self.view];

}

也是用到了trait的属性哇.

要检测3D touch是否被用户关闭/打开了,也是在前面提到的函数里:

traitCollectionDidChange:

4. 不同的size class对应不同的图片尺寸

在横屏,竖屏,以及各种宽高的size class组合下,一张图片需要有不同的尺寸.

这个要是自己用代码写,会多么恶心,要适配多少情况啊,幸好image asset功能之一就是干这个事情的,后面的译文会讲到这个,image asset有配置,可以对不同的size clas配置不同的图片,见图13-2

5. ViewController的present关系用到的size class的改变

这就是我掉坑的重点,了解了何为size class后,vc间的present关系中,presented viewController会在不同的设备环境下有不同的表现. 如在regular环境下(ipad)的popover窗体,到了compact环境下,会变成全屏,而我们可以通过 UIViewController的属性:popoverPresentationController来自定义presented viewController的变现,让它保持非全屏的模式.


译文-终于开始翻译了


原文地址:https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/BuildinganAdaptiveInterface.html

一个可适应的界面应该同时响应trait和size的改变.在view controller这个层级上,用trait来大体决定你要显示的内容和控件们用到的layout. 例如:当在size class之间切换时,你会改变view的属性,显示或者隐藏一些view.

适应trait的改变


trait可让你在不同环境下配置不一样的app显示,大多数配置可以在storyboard上进行,少数还需要代码协助.

用storyboard来配置不同的Size Classes


在IB上使用size classes是很简单的,storyboard编辑器支持在不同的size classes上显示界面.我们可以在特定的size classes上去掉某些view或者更改layout约束,你还可以创建image assets,把不同的image放在不同的size classes上.这样,你就不用用好几套代码来做这种适配屏幕尺寸的事了,当app的size class变化时,UIKit会自动更新对应的界面.(很cool!)

ps:官网上了个老图,为了与时俱进,上最新xcode8的界面


13-1

下面那一排设备,就是不同的size classes,是苹果所有的设备尺寸,还可以选择横屏和竖屏

note: 没安装的view还是在你的view树里面,还可以被操作到,只是不显示罢了

(ps:不是很懂,这里的安装是啥意思?是说在size class-a下我加了个view,size class-b下我让其不显示,但是view还是在么?这样的话,如果多来几套布局,整个包不会很大吗?嘻嘻,不过似乎也只能这样子哟)

image assets 是个很好的存储图片资源的地儿,每个image asset都有一个图片的多个版本,每个版本都有特定的配置.除了可以对普通屏和视网膜屏指定不同的图片,还可以对横屏和竖屏指定不同的图片.只要在image asset里面配置的图片,UIImageView能自动选择和当前的size classes配置相符的图片.(ps:这就是为啥image asset大行其道的原因吧,以前都是放图在bunlde里,现在image asset 会保存多一张图的多个版本,略占空间.但是打包发布到App Store上后,store会采用Slicing技术,针对不同的设备生成不同的app变种,变种的不同图片的选择就是根据image asset来筛选的,所以实际上用户设备中的app是不会很大的. 参考文档:

1. https://developer.apple.com/library/prerelease/content/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html#//apple_ref/doc/uid/TP40012582-CH35-SW3

2. https://onevcat.com/2013/06/new-in-xcode5-and-objc/

)

图13-2展示了image asset的配置,因为又是老图,所以上个新的,好多属性,重要是的是那个width class和height class,通过其组合可以对应不同size classes下的图片.下个帖子一一研究下


13-2

改变子view controller的Traits

默认情况下,子viewController继承父viewController的traits.不过trait和size classes一样,不能要求所有子viewController都和父viewController完全一样.例如:可能给子viewController的显示空间没有那么大,所以一个普通(regular)的父viewController会给它的几个子viewController赋值紧凑(Compact)的size classe.

在容器view controller中,对父viewController调方法:setOverrideTraitCollection:forChildViewController: 来调整子viewController的trait

代码13-1 展示如何创建trait并赋值给子view controller. 这段代码写父viewController中并最好只能执行一次.子view controller的trait会一直保持,不被父viewController影响,直到它从view层中删除.

UITraitCollection*horizTrait=[UITraitCollection

traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];

UITraitCollection*vertTrait=[UITraitCollection

traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];

UITraitCollection*childTraits=[UITraitCollection

traitCollectionWithTraitsFromCollections:@[horizTrait,vertTrait]];

[selfsetOverrideTraitCollection:childTraitsforChildViewController:self.childViewControllers[0]];

问题来啦,trait属性挺多的,被父viewController指定的属性会在子viewController中一直保持,那么没有指定的属性呢?答案是遵循自父viewController的变化,和其保持一致.

如上面这段代码,如果父viewController的水平size class发生变化,可是子viewController还是保持Regular


让Presented View Controllers有新的风格


知识储备

先说在present的意思,苹果里面:

如果一个ViewController  A present了另一个ViewController  B, 则A称为presentingViewController, B称为presentedViewController. 它们通过UIViewController的这2个属性可以访问到彼此.如图1-2


1-2

实际上,presentedViewController是被presentingViewController retain了

ViewController之间的present关系是一种  模态 的关系, B的view覆盖在A的view上,用户点不到A,只能操作B,想要关闭B,只能在B上提供按钮或者navigation item 

在iPhone上present B后,默认情况下,B的view就由自下往上的动画效果展示出来,覆盖在A的view上.

 动画效果是可以改的:presentedViewController的属性modalTransitionStyle

view是覆盖还是替换,还是自定义类似popover的局部的显示? 

用presentedViewController的属性modalPresentationStyle来配置.这里说下:

.FullScreen

默认的机制,presentedViewController的view全屏显示

presentingViewController还是屏幕的根viewcontroller,但是它的view直接被presentedViewController的view替换了.

.OverFullScreen

presentedViewController的view全屏显示

presentingViewController的view还在原来的地方,只是被presentedViewController的view覆盖了,如果presentedViewController的view设置了透明度,还能看到它在下面呢

ios8只支持.FullScreen和OverFullScreen

.PageSheet

presentedViewController的view全屏显示

在iPad 竖屏上,它会略短一截,在statusbar的下面留出空白,ipad横屏和iphone6 pluas上,它左右也会短一截,



present的函数:

open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (@escaping () -> Swift.Void)? = nil)

storyboard的拉线:

Present Modally 和Present as popover ,如图1-3

1-3


关闭的函数(这么说其实不专业,ios里的函数都是消息,发送给对象):

open func dismiss(animated flag: Bool, completion: (@escaping () -> Swift.Void)? = nil)

关闭时,presentingViewController收到该消息,presentedViewController的view从屏幕上撤下,不再覆盖presentingViewController的view,然后presentedViewController的内存被释放. 嘻嘻!别绕晕了,很简单的

值得一提的是:这个消息可以发送给presentingViewController也可以发送给presentedViewController. iOS的runtime会把这个消息最终传递到presentingViewController.所以,最终响应该消息的是presentingViewController

扯起来的话,可说的内容还挺多的,虽然平时关闭一个vc,dismiss就万事大吉,面试时基本也不会有人问,不过多了解一点总是好的.-_-

1. 每个presentingViewController最多只能有一个presentedViewController.如果一个presentingViewController的presentedViewController已经不为nil了.你再给它发送present(:animated:completion:)消息,则不会发生任何反应,completion handler也不会被调用.runtime会给你个警告

2. presentedViewController也能再present view controller, 这就形成了一个链,如果你对中间的B发送dismiss(:completion) 消息,则C会关闭掉,B不会,因为此刻它也是一个presentingViewController ,如图1-4


1-4

3. 如果给一个presentedViewController为nil的view controller发送dismiss(:completion) 消息,则不会发生任何事,但是completion handler会被调用

注意:

1) 默认情况下,presentedViewController覆盖presentingViewController的全部view

1) iOS8后,无论iPhone还是iPad都可让presentedViewController只占据presentingViewController的一个subview的空间,而不是整个view区域

2) IOS7后,无论iPhone还是iPad都可让presentedViewController只占据presentingViewController的一部分区域,而不是整个view区域


继续翻译

presentedViewController 会自适应水平方向上的的regular和compact.比如:当从水平的regular变成水平的compact时,UIKit默认自动把ViewController的presentation style变成了UIModalPresentationFullScreen,自定义presentation style是由presentation controller进行调整的.

啰嗦几句:

presentation controller和presentation style是啥呢?

UIViewController有个扩展,写了2个只能get的属性:

1) presentationController

2) popoverPresentationController  (它其实继承于presentationController,是presentationController的popover的扩展)

它们不是继承于UIViewController的,而是继承于NSObject的.它们负责管理一个UIViewController的present的某些属性.

注意:

一定要先给UIViewController的modalPresentationStyle属性赋值,上面2个属性才不会是nil的.

1) 如果modalPresentationStyle是.popover,则popoverPresentationController被赋值

2) 如果modalPresentationStyle是其他枚举值,则presentationController被赋值

继续翻译

对于某些app,全屏的present会造成困扰.比如,如果点击一个popover的bounds的外面空白地方,会关掉这个popover,但是如果这个popover已经占领了全屏,就没法关闭它了,就如13-3所示.如果默认的自适应style机制不能满足你的需要,你应该告知UIKit用不同的style来present

13-3 popover从regular变成compact时,popover变成了全屏

如何自定义presenting style呢? 首先得给presentedViewController的presentationController或者popoverPresentationController设置delegate,当自适应变化开始时,delegate的方法会被触发.它会返回不同的presenting style,它还可以让presentationController交替显示不同的ViewController(-_- delegate略强大)

在delegate中实现UIAdaptivePresentationControllerDelegate的代理方法:

optional public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle

头文件的注释道:在ios8里,只支持2个style

UIModalPresentationFullScreen:全屏,

UIModalPresentationOverFullScreen.

注意吧.官网上写的是:adaptivePresentationStyleForPresentationController: 如果真直接copy过去实现这个,就完蛋了,我就栽在这里了.

代码:

//MARK: UIPopoverPresentationControllerDelegate

func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle{

return .none

}

当app转入compact的环境时,只支持全屏或者 无 (UIModalPresentationNone),返回.none就是告诉presentation controller忽略compact的环境,保持之前的present style,在任何设备上都如此,13-4展示了全屏和非全屏的效果,以做比较:


13-4

delegate的另一个方法

public func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController?

当进入了compact环境时,你可以在你的view层次里insert一个navigation controller,或者load一个新的view controller,来替换本该present的view controller

(不好意思,在iphone6上没调试出来,实现了没进入过该函数,我还要进一步研究)


实现适应性弹出框(Popover)的建议


当从水平Regular变成水平Compact环境时,Pop over 需要额外的改动.水平Compact环境下,默认的行为是popover会变成全屏.因为popover的关闭方法是点击它bounds之外的空白处,而全屏就无法做到这一点.我们可以通过下面的做法来改变这种情况:

1) 让popover到navigation的栈里面

加一个navigation controller,通过navigationBarItem来dismiss它

2) 加控件来dismiss全屏的popover

可以通过加控件来dismiss全屏的popover,但是更多的办法是用navigation controller换掉popover, 函数是:

presentationController:viewControllerForAdaptivePresentationStyle:

用navigation controller给你的模态界面加上一个Done按钮或者其他控件来dismiss掉这个界面.

3) 用presentation controller的delegate消灭一切系统的的自适应

这就是网上大多数的帖子做法,他们经常通过简洁的方式,直接指导你实现步骤,但是不说为啥,对看帖子的人来说,提升不大,换而言之,看的人应该多思考多查阅,不能让别人把什么都摆在你面前,那也做不到.除非是时间来不及,项目逼死人.--说给自己听的!自勉!

作为popover的UIViewController,设置它的属性popoverPresentationController的delegate

实现delegate的func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle方法,返回:UIModalPresentationNone (.none) 更多的信息,请参考:https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/BuildinganAdaptiveInterface.html#//apple_ref/doc/uid/TP40007457-CH32-SW6

对Size 的变化做出反应

size会因为很多原因发生变化,包括下面的:

1) 窗口的尺寸变化,大多数时候由转动屏幕引起

2) 父view controller 重新设置了子view controller的尺寸

3) presentationController改变了它显示的ViewController的尺寸

当size发生变化时,UIKit根据layout的约束,自动改变当前显示的view controller层级的的尺寸和位置,如果你使用auto layout来指定view的size和位置,你的app会自动适应任何size和设备带来的变化.

如果你的autoulayout 还不足以达到你所想要的效果, 你可以用viewWillTransitionToSize:withTransitionCoordinator: 来改变你的布局,你可以创建额外的动画,这些动画会和size-change的动画一起运行.例如:当转屏时,调用UIViewControllerTransitionCoordinator的属性targetTransform 来创建一个反转矩阵给某些需要的显示控件使用


demo:https://github.com/ivychenyucong/TestPopover

参考: http://www.cnblogs.com/zhw511006/p/3998534.html   对ios8的size classes讲解的很清楚到位

https://onevcat.com/2014/07/ios-ui-unique/  size class的解释很清楚

翻译