写在之前的大篇废话
研究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的方法,所幸UIViewController和UIPresentationController都实现了它. 它也是在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-43. 如果给一个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-4delegate的另一个方法
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的解释很清楚
网友评论