0%

iOS屏幕旋转知识点以及实现

iOS屏幕旋转

此文主要针对IOS应用, 是屏幕旋转相关问题的一个总结. 主要内容有:

  • iOS4,5,6-9,10+不同版的适配.
  • 强制旋转和自动旋转.
  • 个别屏幕可以旋转,其他屏幕不能旋转

两种orientation

了解屏幕旋转首先需要区分两种orientation

Device Orientation

设备的物理方向,由类型UIDeviceOrientation表示,当前设备方向获取方式:

[UIDevice currentDevice].orientation

该属性的值一般是与当前设备方向保持一致的,但须注意以下几点:

  1. 文档中对该属性的注释:
1
2
3
// return current device orientation.
// this will return UIDeviceOrientationUnknown unless device orientation notifications are being generated.
@property(nonatomic,readonly) UIDeviceOrientation orientation;

所以更推荐下面这种用法:

1
2
3
4
5
6
if (![UIDevice currentDevice].generatesDeviceOrientationNotifications) {
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
}
NSLog(@"%d",[UIDevice currentDevice].orientation);

[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
  1. 系统横竖屏开关关闭时

如果关闭了系统的横竖屏切换开关,即系统层级只允许竖屏时,再通过上述方式获取到的设备方向将永远是UIDeviceOrientationUnknown。可以通过Core Motion中的CMMotionManager来获取当前设备方向。

Interface Orientation

界面显示的方向,由类型UIInterfaceOrientation表示。当前界面显示方向有以下两种方式获取:

1
2
NSLog(@"%d",[UIApplication sharedApplication].statusBarOrientation);
NSLog(@"%d",viewController.interfaceOrientation);

即可以通过系统statusBar的方向或者viewController的方向来获取当前界面方向。

二者区别

通过UIDevice获取到的设备方向在手机旋转时是实时的,通过UIApplicationstatusBar或者viewController获取到的界面方向在下述方法:

1
2
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation 
duration:(NSTimeInterval)duration

调用以后才会被更改成最新的值。

相关枚举定义

目前屏幕旋转相关的枚举变量定义主要包括以下三种:

  • UIDeviceOrientation硬件设备屏幕方向
  • UIInterfaceOrientation程序界面旋转方向
  • UIInterfaceOrientationMask程序界面旋转方向集合。

相信有一部分人在开发过程中对三个枚举变量类型没有明确的区分概念,甚至混用。

UIDeviceOrientation

1
2
3
4
5
6
7
8
9
typedef NS_ENUM(NSInteger, UIDeviceOrientation) {
UIDeviceOrientationUnknown,
UIDeviceOrientationPortrait, // Device oriented vertically, home button on the bottom
UIDeviceOrientationPortraitUpsideDown, // Device oriented vertically, home button on the top
UIDeviceOrientationLandscapeLeft, // Device oriented horizontally, home button on the right
UIDeviceOrientationLandscapeRight, // Device oriented horizontally, home button on the left
UIDeviceOrientationFaceUp, // Device oriented flat, face up
UIDeviceOrientationFaceDown // Device oriented flat, face down
} __TVOS_PROHIBITED;

iOS 6 之前用于控制屏幕方向的枚举UIInterfaceOrientation

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
UIInterfaceOrientationUnknown = UIDeviceOrientationUnknown,
UIInterfaceOrientationPortrait = UIDeviceOrientationPortrait,
UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
} __TVOS_PROHIBITED;

从宏定义可知,UIDeviceOrientation方向比UIInterfaceOrientation多了两个定义:UIDeviceOrientationFaceUpUIDeviceOrientationFaceDown,分别表示手机水平放置,屏幕向上和屏幕向下。

iOS 6 及之后版本用于控制屏幕方向的枚举UIInterfaceOrientationMask

1
2
3
4
5
6
7
8
9
typedef NS_OPTIONS(NSUInteger, UIInterfaceOrientationMask) {
UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),
UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),
UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),
UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),
UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),
UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
} __TVOS_PROHIBITED;
  • iOS 6 及之后版本使用的 UIInterfaceOrientationMask 类型来控制屏幕屏幕方向,该类型也新增加了几个枚举取值,可用一个枚举取值来代表多个屏幕方向。

  • 四个基本屏幕方向(上、下、左、右)中,UIInterfaceOrientationMask = (1 << UIInterfaceOrientation),所以,如果你的应用中需要动态的将 UIInterfaceOrientation 类型转换成 UIInterfaceOrientationMask 类型的话,只需做一下上面的转换即可,不需要通过 switch 来判断再转换。

改变Orientation的三种途径

这里主要理清一下:到底有哪些设置可以改变屏幕旋转特性,这样的话

  • 出现任何问题我们都可以从这几个途径中发现原因;
  • 灵活应付产品经理的各种需求

首先我们得知道:

  1. 当手机的重力感应打开的时候,如果用户旋转手机,系统会抛发UIDeviceOrientationDidChangeNotification 事件;

  2. 您可以分别设置ApplicationUIViewcontroller支持的旋转方向;Application的设置会影响整个App,UIViewcontroller的设置仅仅会影响一个viewController(IOS5和IOS6有所不同,下面会详细解释);

  3. UIKit收到UIDeviceOrientationDidChangeNotification事件的时候,会根据ApplicationUIViewcontroller的设置, 如果双方都支持此方向, 则会自动屏幕旋转到这个方向(会对两个设置求与运算得到可以支持的方向);如果求与之后,没有任何可支持的方向,则会抛发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
2
3
4
5
6
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>

此配置其实跟工程中 TargetSummary 界面中的 Device Orientation 配置是一致的,修改任意一边,另一个边都会同步的修改。

eg:

setting

UIWindow的方向控制(iOS6及以上版本才有效)

iOS6UIApplicationDelegate提供了下述方法,能够指定 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
2
// Applications should use supportedInterfaceOrientations and/or shouldAutorotate..
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation NS_DEPRECATED_IOS(2_0, 6_0);

如果打算支持toInterfaceOrientation对应的方向就返回YES,否则返回NO。默认情况下,此方法只有参数为 UIInterfaceOrientationPortrait 时,返回值才为真,即默认只支持竖屏向上。下面的例子中,表示支持横屏向右及横屏向左两个方向。

eg:

1
2
3
4
5
6
// 是否支持旋转到某个屏幕方向
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
return ((toInterfaceOrientation == UIInterfaceOrientationLandscapeRight) |
(toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft));
}
iOS6以及以后版本中控制屏幕旋转相关方法:

限制

  • 当前controllerwindowrootViewController
  • 当前controllermodal模式的(present出来的)

iOS10以及之前版本变化

  • iOS6~iOS9modal的controller需要rootViewController做特殊处理调用旋转
  • iOS10+ modelcontroller可以独立处理

只有以上两种情况时候,以下的orientations相关方法才会起作用(才会被调用),当前controller及其所有的childViewController都在此作用范围内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// New Autorotation support.
// 是否支持转屏
// @property(nonatomic, readonly) BOOL shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
- (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;


// 支持的屏幕方向,此处可直接返回 UIInterfaceOrientationMask 类型
// 也可以返回多个 UIInterfaceOrientationMask 取或运算后的值
- (UIInterfaceOrientationMask)supportedInterfaceOrientations NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;


// Returns interface orientation masks.
// 优先方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
  • - shouldAutorotate

    方法决定是否支持多方向旋转屏,如果返回NO则后面的两个方法都不会再被调用,而且只会支持默认的UIInterfaceOrientationMaskPortrait方向;

  • - supportedInterfaceOrientations

    直接返回支持的旋转方向,该方法在iPad上的默认返回值UIInterfaceOrientationMaskAll,iPhone上的默认返回值是UIInterfaceOrientationMaskAllButUpsideDown,详情见官方Q&A文档

  • - preferredInterfaceOrientationForPresentation

    返回最优先显示的屏幕方向,比如同时支持PortraitLandscape方向,但想优先显示Landscape方向,那软件启动的时候就会先显示Landscape,在手机切换旋转方向的时候仍然可以在PortraitLandscape之间切换;

attemptRotationToDeviceOrientation方法

从iOS5开始有了这个新方法:

1
2
3
4
// call this method when your return value from shouldAutorotateToInterfaceOrientation: changes
// if the current interface orientation does not match the current device orientation,
// a rotation may occur provided all relevant view controllers now return YES from shouldAutorotateToInterfaceOrientation:
+ (void)attemptRotationToDeviceOrientation NS_AVAILABLE_IOS(5_0);

该方法的使用场景是Interface OrientationDevice 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 的异常,然后直接崩溃,所以还是要保持这三个值的交集为非空。

强制屏幕旋转

如果InterfaceDevice方向不一样,想强制将Interface旋转成Device的方向,可以通过attemptRotationToDeviceOrientation实现,但是如果想将Interface强制旋转成任一指定方向,该方式就无能为力了。

不过聪明的开发者们总能想到解决方式:

私有方法

1
[[UIDevice currentDevice] setOrientation:UIInterfaceOrientationPortrait];

但是现在苹果已经将该方法私有化了,越狱开发的同学可以试试,或者自己想法子骗过苹果审核吧。

可能骗过审核的方法:

  • objc: NSInvocation
1
2
3
4
5
6
7
8
9
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
SEL selector = NSSelectorFromString(@"setOrientation:");
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:[UIDevice currentDevice]];
int val = UIInterfaceOrientationLandscapeRight;
[invocation setArgument:&val atIndex:2];
[invocation invoke];
}
  • objc: performSelector: withObject:
1
2
3
4
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {  
[[UIDevice currentDevice] performSelector:@selector(setOrientation:)
withObject:(id)UIInterfaceOrientationLandscapeRight];
}
  • swift: setValue(_ , forKey: String)
1
UIDevice.current.setValue(UIDeviceOrientation.portrait.rawValue, forKey: "orientation")

旋转view的transform

也可以通过旋转viewtransform属性达到强制旋转屏幕方向的目的,但个人感觉这不是靠谱的思路,可能会带来某些诡异的问题。

1
2
3
4
5
UIView.animate(withDuration: UIApplication.shared.statusBarOrientationAnimationDuration) {
self.view.transform = CGAffineTransform.identity
self.view.transform = CGAffineTransform.init(rotationAngle: CGFloat(Double.pi / 2))
self.view.bounds = CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.height, height: UIScreen.main.bounds.width)
}

主动触发Orientation机制

要是能主动触发系统的orientation机制,调用orientation相关方法,使新设置的orientation值起作用就好了。这样只要提前设置好想要支持的orientation,然后主动触发orientation机制,便能实现将interface orientation旋转至任意方向的目的。

万能的stackoverflow上提供了一种主动触发的方式:

iOS4iOS6以后:

1
2
3
4
UIViewController *vc = [[UIViewController alloc]init];
[self presentModalViewController:vc animated:NO];
[self dismissModalViewControllerAnimated:NO];
[vc release];

iOS5中:

1
2
3
4
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIView *view = [window.subviews objectAtIndex:0];
[view removeFromSuperview];
[window addSubview:view];

这种方式会触发UIKit重新调用ControllerOrientation相关方法,以达到在Device方向不变的情况下改变Interface方向的目的。

实现并兼容iOS6(8)以后所有版本

  • 需求:

    1. 需要在iOS8-iOS10中支持个别屏幕可以自由旋转
    2. 项目主体是竖屏
  • 思路

    1. rootViewController去获得当前显示的ViewController是否允许旋转
    2. 所有controller继承基controller,基controller不允许旋转,特殊的controller特殊处理
  • swift实现,iOS8以后所有版本,iOS8之前objc实现即可

设置全局方向控制

  • 项目勾选(全局):
  • AppDelegate实现代理(UIWindow):

    返回与勾选的类型一致的值,也可以不进行实现,默认一致

1
2
3
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.allButUpsideDown
}

定义协议OrientationProtocol并对UIViewController扩展实现

协议为了方便各个子viewcontroller自行控制自己的方向,只需要重写协议内的函数实现即可,无需在控制原有的几个orientation函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// MARK: OrientationProtocol viewcontroller旋转控制
protocol OrientationProtocol {
// supportedInterfaceOrientations
func orientations() -> UIInterfaceOrientationMask
// shouldAutorotate
func autorotate() -> Bool
// preferredInterfaceOrientationForPresentation
func preferredOrientation() -> UIInterfaceOrientation
}

extension UIViewController: OrientationProtocol {
func orientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.portrait
}

func autorotate() -> Bool {
return true
}

func preferredOrientation() -> UIInterfaceOrientation {
return UIInterfaceOrientation.portrait
}
}

设置BaseViewController

为了使所有的controller默认竖屏而不需要每个controller都进行设置

  • 所有的ViewController继承BaseViewController
  • 所有的TableVIewController继承BaseTableViewController
  • 所有的BaseController实现以下扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
extension BaseViewController {
override var shouldAutorotate: Bool {
return true
}

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.portrait
}

override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return UIInterfaceOrientation.portrait
}
}

修改window所有的rootViewController

  • AppDelegat.getCurrentViewController()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class AppDelegate: UIResponder, UIApplicationDelegate {
........
........

// 获取当前屏幕显示的viewcontroller
static func getCurrentViewController() -> UIViewController? {
// 嵌套函数,寻找当前屏幕上显示的viewcontroller
func findBestViewController(from viewController: UIViewController?) -> UIViewController? {
if let presentedViewController = viewController?.presentedViewController {
// 返回presentedViewController
return findBestViewController(from: presentedViewController)
} else if let navigationViewController = viewController as? UINavigationController {
// 返回最顶部的vc
if navigationViewController.viewControllers.count > 0 {
return findBestViewController(from: navigationViewController.topViewController)
} else {
return navigationViewController
}
} else if let tabBarController = viewController as? UITabBarController {
// 返回当前选择的vc
if (tabBarController.viewControllers?.count ?? 0) > 0 {
return findBestViewController(from: tabBarController.selectedViewController)
} else {
return tabBarController
}
} else {
// 普通vc返回自己
return viewController
}
}

let rootViewController = UIApplication.shared.keyWindow?.rootViewController
return findBestViewController(from: rootViewController)
}

........
........
}
  • RootViewController设置获取当前显示的controller进行变换旋转值的设置

    这里兼容了iOS10之前的modal的controller需要rootViewController设置方向的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// MARK: 项目的RootViewController是MainTabBarController
extension MainTabBarController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if let vc = AppDelegate.getCurrentViewController() {
return vc.orientations()
}
return UIInterfaceOrientationMask.portrait
}

override var shouldAutorotate: Bool {
if let vc = AppDelegate.getCurrentViewController() {
return vc.autorotate()
}
return true
}

override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
if let vc = AppDelegate.getCurrentViewController() {
return vc.preferredOrientation()
}
return UIInterfaceOrientation.portrait
}
}

设置个别的可以旋转方向的controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension SomeSpecialViewController {
override func orientations() -> UIInterfaceOrientationMask {
// 返回允许的方向
return UIInterfaceOrientationMask.allButUpsideDown
}

override func autorotate() -> Bool {
// 设置允许自动旋转
return true
}

override func preferredOrientation() -> UIInterfaceOrientation {
// 设置优先的方向
return UIInterfaceOrientation.portrait
}
}

遇到的问题

在实现过程中发现,如果不设置BaseViewController中的方法,只进行UIViewController扩展,会出现一种情况:

从一个不可旋转的页面进入一个可旋转页面后,旋转到其他方向,返回上一个页面,这时候上一个页面也转动了….

所以使用baseViewController的方式进行锁死不可旋转页面的方向,目前没发现其他的更好的处理方式。





参考资料:

  1. How to lock view controller in portrait orientation?
  2. iOS屏幕旋转学习笔记
  3. iOS屏幕旋转二三事(Orientations)
  4. iOS 屏幕方向那点事儿
  5. IOS Orientation, 想怎么转就怎么转~
  6. How to force a UIViewController to Portrait orientation in iOS 6
  7. Why won’t my UIViewController rotate with the device?
  8. iOS两个强制旋转屏幕的方法