iOS屏幕旋转
此文主要针对IOS应用, 是屏幕旋转相关问题的一个总结. 主要内容有:
- iOS4,5,6-9,10+不同版的适配.
- 强制旋转和自动旋转.
- 个别屏幕可以旋转,其他屏幕不能旋转
两种orientation
了解屏幕旋转首先需要区分两种orientation
Device Orientation
设备的物理方向,由类型UIDeviceOrientation表示,当前设备方向获取方式:
[UIDevice currentDevice].orientation
该属性的值一般是与当前设备方向保持一致的,但须注意以下几点:
- 文档中对该属性的注释:
1 | // return current device orientation. |
所以更推荐下面这种用法:
1 | if (![UIDevice currentDevice].generatesDeviceOrientationNotifications) { |
- 系统横竖屏开关关闭时
如果关闭了系统的横竖屏切换开关,即系统层级只允许竖屏时,再通过上述方式获取到的设备方向将永远是UIDeviceOrientationUnknown
。可以通过Core Motion
中的CMMotionManager
来获取当前设备方向。
Interface Orientation
界面显示的方向,由类型UIInterfaceOrientation
表示。当前界面显示方向有以下两种方式获取:
1 | NSLog(@"%d",[UIApplication sharedApplication].statusBarOrientation); |
即可以通过系统statusBar
的方向或者viewController
的方向来获取当前界面方向。
二者区别
通过UIDevice
获取到的设备方向在手机旋转时是实时的,通过UIApplication
的statusBar
或者viewController
获取到的界面方向在下述方法:
1 | - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation |
调用以后才会被更改成最新的值。
相关枚举定义
目前屏幕旋转相关的枚举变量定义主要包括以下三种:
UIDeviceOrientation
硬件设备屏幕方向UIInterfaceOrientation
程序界面旋转方向UIInterfaceOrientationMask
程序界面旋转方向集合。
相信有一部分人在开发过程中对三个枚举变量类型没有明确的区分概念,甚至混用。
UIDeviceOrientation
1 | typedef NS_ENUM(NSInteger, UIDeviceOrientation) { |
iOS 6 之前用于控制屏幕方向的枚举UIInterfaceOrientation
1 | typedef NS_ENUM(NSInteger, UIInterfaceOrientation) { |
从宏定义可知,
UIDeviceOrientation
方向比UIInterfaceOrientation
多了两个定义:UIDeviceOrientationFaceUp
和UIDeviceOrientationFaceDown
,分别表示手机水平放置,屏幕向上和屏幕向下。
iOS 6 及之后版本用于控制屏幕方向的枚举UIInterfaceOrientationMask
1 | typedef NS_OPTIONS(NSUInteger, UIInterfaceOrientationMask) { |
iOS 6 及之后版本使用的 UIInterfaceOrientationMask 类型来控制屏幕屏幕方向,该类型也新增加了几个枚举取值,可用一个枚举取值来代表多个屏幕方向。
四个基本屏幕方向(上、下、左、右)中,UIInterfaceOrientationMask = (1 << UIInterfaceOrientation),所以,如果你的应用中需要动态的将 UIInterfaceOrientation 类型转换成 UIInterfaceOrientationMask 类型的话,只需做一下上面的转换即可,不需要通过 switch 来判断再转换。
改变Orientation的三种途径
这里主要理清一下:到底有哪些设置可以改变屏幕旋转特性,这样的话
- 出现任何问题我们都可以从这几个途径中发现原因;
- 灵活应付产品经理的各种需求
首先我们得知道:
当手机的重力感应打开的时候,如果用户旋转手机,系统会抛发
UIDeviceOrientationDidChangeNotification
事件;您可以分别设置
Application
和UIViewcontroller
支持的旋转方向;Application
的设置会影响整个App,UIViewcontroller
的设置仅仅会影响一个viewController
(IOS5和IOS6有所不同,下面会详细解释);当
UIKit
收到UIDeviceOrientationDidChangeNotification
事件的时候,会根据Application
和UIViewcontroller
的设置, 如果双方都支持此方向, 则会自动屏幕旋转到这个方向(会对两个设置求与运算得到可以支持的方向);如果求与之后,没有任何可支持的方向,则会抛发UIApplicationInvalidInterfaceOrientationException
异常.
如何决定Interface Orientation
全局控制
Info.plist
文件中,有一个Supported Interface Orientations
,可以配置整个应用的屏幕方向,此处为全局控制。
key | xcode name | Summary | avilable value |
---|---|---|---|
UIInterfaceOrientation | initial interface orientation | Specifies the initial orientation of the app’s user interface. | UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight |
UISupportedInterfaceOrientations | Supported interface orientations | Specifies the orientations that the app supports. | UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight |
在Info.plist中设置之后,这个app里所有的viewController支持的自动旋转方向都只能是app支持的方向的子集.
eg:
1 | <key>UISupportedInterfaceOrientations</key> |
此配置其实跟工程中 Target
的 Summary
界面中的 Device Orientation
配置是一致的,修改任意一边,另一个边都会同步的修改。
eg:
UIWindow
的方向控制(iOS6及以上版本才有效)
iOS6
的UIApplicationDelegate
提供了下述方法,能够指定 UIWindow 中的界面的屏幕方向:
1 | - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(nullable UIWindow *)window NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED; |
该方法默认值为Info.plist
中配置的Supported Interface Orientations
项的值。
iOS
中通常只有一个window
,所以此处的控制也可以视为全局控制。
UIViewController
的方向控制
iOS5中控制屏幕旋转的方法:
在 iOS 6 之前,单个界面的屏幕方向控制,都使用 UIViewController 类中的这个方法:
1 | // Applications should use supportedInterfaceOrientations and/or shouldAutorotate.. |
如果打算支持toInterfaceOrientation
对应的方向就返回YES,否则返回NO。默认情况下,此方法只有参数为 UIInterfaceOrientationPortrait 时,返回值才为真,即默认只支持竖屏向上。下面的例子中,表示支持横屏向右及横屏向左两个方向。
eg:
1 | // 是否支持旋转到某个屏幕方向 |
iOS6以及以后版本中控制屏幕旋转相关方法:
限制
- 当前
controller
是window
的rootViewController
- 当前
controller
是modal
模式的(present
出来的)
iOS10以及之前版本变化
iOS6
~iOS9
中modal的controller
需要rootViewController
做特殊处理调用旋转iOS10+
model
的controller
可以独立处理
只有以上两种情况时候,以下的orientations
相关方法才会起作用(才会被调用),当前controller
及其所有的childViewController
都在此作用范围内。
1 | // New Autorotation support. |
- shouldAutorotate
方法决定是否支持多方向旋转屏,如果返回NO则后面的两个方法都不会再被调用,而且只会支持默认的
UIInterfaceOrientationMaskPortrait
方向;- supportedInterfaceOrientations
直接返回支持的旋转方向,该方法在iPad上的
默认返回值
是UIInterfaceOrientationMaskAll
,iPhone上的默认返回值是UIInterfaceOrientationMaskAllButUpsideDown
,详情见官方Q&A文档;- preferredInterfaceOrientationForPresentation
返回最优先显示的屏幕方向,比如同时支持
Portrait
和Landscape
方向,但想优先显示Landscape
方向,那软件启动的时候就会先显示Landscape
,在手机切换旋转方向的时候仍然可以在Portrait
和Landscape
之间切换;
attemptRotationToDeviceOrientation方法
从iOS5开始有了这个新方法:
1 | // call this method when your return value from shouldAutorotateToInterfaceOrientation: changes |
该方法的使用场景是Interface Orientation
和Device Orientation
不一致,但希望通过重新指定Interface Orientation
的值,立即实现二者一致;如果这时只是更改了支持的Interface Orientation
的值,没有调用attemptRotationToDeviceOrientation
,那么下次Device Orientation
变化的时候才会实现二者一致,关键点在于能不能立即实现。
举个例子:
假设当前的
Interface Orientation
只支持Portrait
,如果Device Orientation
变成Landscape
,那么Interface Orientation
仍然显示Portrait
;如果这时我们希望
Interface Orientation
也变成和Device Orientation
一致的Landscape
,以iOS6为例,需要先将supportedInterfaceOrientations
的返回值改成Landscape
,然后调用attemptRotationToDeviceOrientation
方法,系统会重新询问支持的Interface Orientation
,已达到立即更改当前Interface Orientation
的目的。
最终支持的屏幕方向
前面所述的3种控制规则的交集就是一个controller的最终支持的方向,就是说:一个界面最后支持的屏幕方向,是取
(全局控制 ∩ UIWindow 中的界面控制 ∩ 单个界面控制)
的交集,如果全局控制支持所有屏幕方向,UIWindow
中的界面控制支持横屏,当个界面中只是支持横屏向右,那么最后界面只会以横屏向右显示,并且不支持旋转到其他的方向。如果以上三种控制支持的屏幕方向最后的交集为空,iOS 5 跟 iOS 6 的处理有点不同,在 iOS 6 下,甚至会直接抛出
UIApplicationInvalidInterfaceOrientationException
的异常,然后直接崩溃,所以还是要保持这三个值的交集为非空。
强制屏幕旋转
如果Interface
和Device
方向不一样,想强制将Interface
旋转成Device
的方向,可以通过attemptRotationToDeviceOrientation
实现,但是如果想将Interface
强制旋转成任一指定方向,该方式就无能为力了。
不过聪明的开发者们总能想到解决方式:
私有方法
1 | [[UIDevice currentDevice] setOrientation:UIInterfaceOrientationPortrait]; |
但是现在苹果已经将该方法私有化了,越狱开发的同学可以试试,或者自己想法子骗过苹果审核吧。
可能骗过审核的方法:
- objc:
NSInvocation
1 | if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) { |
- objc:
performSelector: withObject:
1 | if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) { |
- swift:
setValue(_ , forKey: String)
1 | UIDevice.current.setValue(UIDeviceOrientation.portrait.rawValue, forKey: "orientation") |
旋转view的transform
也可以通过旋转view
的transform
属性达到强制旋转屏幕方向的目的,但个人感觉这不是靠谱的思路,可能会带来某些诡异的问题。
1 | UIView.animate(withDuration: UIApplication.shared.statusBarOrientationAnimationDuration) { |
主动触发Orientation机制
要是能主动触发系统的orientation机制,调用orientation相关方法,使新设置的orientation值起作用就好了。这样只要提前设置好想要支持的orientation,然后主动触发orientation机制,便能实现将interface orientation旋转至任意方向的目的。
万能的stackoverflow上提供了一种主动触发的方式:
在iOS4
和iOS6
以后:
1 | UIViewController *vc = [[UIViewController alloc]init]; |
iOS5
中:
1 | UIWindow *window = [[UIApplication sharedApplication] keyWindow]; |
这种方式会触发UIKit重新调用Controller
的Orientation
相关方法,以达到在Device
方向不变的情况下改变Interface
方向的目的。
实现并兼容iOS6(8)以后所有版本
需求:
- 需要在iOS8-iOS10中支持个别屏幕可以自由旋转
- 项目主体是竖屏
思路
- rootViewController去获得当前显示的ViewController是否允许旋转
- 所有controller继承基controller,基controller不允许旋转,特殊的controller特殊处理
swift实现,iOS8以后所有版本,iOS8之前objc实现即可
设置全局方向控制
- 项目勾选(全局):
AppDelegate实现代理(UIWindow):
返回与勾选的类型一致的值,也可以不进行实现,默认一致
1 | func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { |
定义协议OrientationProtocol并对UIViewController扩展实现
协议为了方便各个子viewcontroller自行控制自己的方向,只需要重写协议内的函数实现即可,无需在控制原有的几个orientation函数
1 | // MARK: OrientationProtocol viewcontroller旋转控制 |
设置BaseViewController
为了使所有的controller默认竖屏而不需要每个controller都进行设置
- 所有的ViewController继承BaseViewController
- 所有的TableVIewController继承BaseTableViewController
- 所有的BaseController实现以下扩展
1 | extension BaseViewController { |
修改window所有的rootViewController
AppDelegat.getCurrentViewController()
1 | class AppDelegate: UIResponder, UIApplicationDelegate { |
- RootViewController设置获取当前显示的controller进行变换旋转值的设置
这里兼容了iOS10之前的modal的controller需要rootViewController设置方向的问题
1 | // MARK: 项目的RootViewController是MainTabBarController |
设置个别的可以旋转方向的controller
1 | extension SomeSpecialViewController { |
遇到的问题
在实现过程中发现,如果不设置BaseViewController中的方法,只进行UIViewController扩展,会出现一种情况:
从一个不可旋转的页面进入一个可旋转页面后,旋转到其他方向,返回上一个页面,这时候上一个页面也转动了….
所以使用baseViewController的方式进行锁死不可旋转页面的方向,目前没发现其他的更好的处理方式。
参考资料: