在最近的开发中测试提交了个Bug:"打开视频播放并将视频横屏播放,将App退到后台,通过widget入口打开App,播放的视频关闭,结果App主页面显示横屏",这个问题让我有看了下iOS设置转屏的相关内容,个人觉得有必要总结下,将结果落实在笔头上有助于记忆!
转屏的实现机制
- 转屏基础
加速计是实现iOS转屏的基础。依赖加速计才可以判断出设备当前的方向,通过加速计检测设备方向变化,将会发出UIDeviceOrientationDidChangeNotification这个通知,通知App设备方向发生了变化,注册这个通知后在通知对应的方法中处理转屏后的UI相关操作即可。
- 设备旋转的时候
UIKit
会受到转屏事件的通知 -
UIKit
通过AppDelegate
通知当前的Window
,并设置支持的屏幕方向; -
window
通知ViewController
的转屏事件,判断该viewController
所支持的旋转方向,完成旋转;(如果页面结构为viewController
→Navigation
→UITabbarController
判断有差异,后面会介绍) - 如果存在模态弹出的
viewController
的话,系统则会根据此ViewController的设置来判断是否要进行旋转;
UIDeviceOrientationDidChangeNotification
这个通知是设备的物理方向发生变化时会被发送,有时手机屏幕页面没有变化旋转的需求,但是此通知仍被发送,所以这时会引起一些问题
- 加速计判断屏幕方向
self.motionManager = [[CMMotionManager alloc] init];
if (![self.motionManager isGyroAvailable]) {
NSLog(@"CMMotionManager 陀螺仪不可用");
}
else{
// 3.设置陀螺仪更新频率,以秒为单位
self.motionManager.gyroUpdateInterval = 0.1;
// 4.开始实时获取
[self.motionManager startGyroUpdatesToQueue:[[NSOperationQueue alloc] init] withHandler:^(CMGyroData * _Nullable gyroData, NSError * _Nullable error) {
//获取陀螺仪数据
CMRotationRate rotationRate = gyroData.rotationRate;
NSLog(@"CMMotionManager 加速度 == x:%f, y:%f, z:%f", rotationRate.x, rotationRate.y, rotationRate.z);
}];
}
转屏相关枚举值:
一共有三个枚举值,下面就一一简单的介绍下
- UIDeviceOrientation:
UIDeviceOrientation
指的是硬件设备当前的旋转方向,判断设备方向以Home键作为参考,这个枚举在UIDevive.h
中。
- 枚举内容
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
} API_UNAVAILABLE(tvOS);
- 获取设备旋转方向的方法
[UIDevice currentDevice].orientation
-
orientation
这个属性是只读的,设备方向是只能取值,不能设置值
@property(nonatomic,readonly) UIDeviceOrientation orientation;
- 监听设备方向变化
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleDeviceOrientationChange) name:UIDeviceOrientationDidChangeNotification object:nil];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
- (BOOL)handleDeviceOrientationChange{
UIDeviceOrientation *deviceOrientation = [UIDevice currentDevice].orientation
//根据设备方向处理不同的情况
//省略.....
}
APP可以选择性的是否接收UIDeviceOrientationDidChangeNotification
通知。
相关属性:
// 是否已经开启了设备方向改变的通知
@property(nonatomic,readonly,getter=isGeneratingDeviceOrientationNotifications) BOOL generatesDeviceOrientationNotifications;
// 开启接收接收 UIDeviceOrientationDidChangeNotification 通知
- (void)beginGeneratingDeviceOrientationNotifications;
// 结束接收接收 UIDeviceOrientationDidChangeNotification 通知
- (void)endGeneratingDeviceOrientationNotifications;
在 app 代理里面结束接收 设备旋转的通知事件, 后续的屏幕旋转监听都会失效
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 结束接收接收 UIDeviceOrientationDidChangeNotification 通知
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
return YES;
}
- UIInterfaceOrientation 页面的显示方向:
UIInterfaceOrientation
指的是页面当前旋转的方向,页面方向是可以设置的,在系统锁屏按钮开启的状态下依旧可以通过强制转屏来实现屏幕旋转。这个枚举值定义在UIApplication.h中。
- 枚举类型
typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
UIInterfaceOrientationUnknown = UIDeviceOrientationUnknow, UIInterfaceOrientationPortrait = UIDeviceOrientationPortrait,
UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
}
通过UIInterfaceOrientation
和UIDeviceOrientation
对比发现两者之间大部分的枚举值是可以对应上的,只有以下两个枚举值是相反的:
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
当设备向左转时屏幕是需要向右转的,当设备向右转时屏幕是需要向左转的;
- 获取页面方向的方式
iOS 8之前
//读取UIViewController的interfaceOrientation属性
@property(nonatomic,readonly) UIInterfaceOrientation
iOS 13之前,获取状态条方向
[UIApplication sharedApplication].statusBarOrientation
iOS13之后,获取状态条方向
[UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation;
获取页面方向的方法封装
- (UIInterfaceOrientation)currentInterfaceOrientation
{
if (@available(iOS 13.0 , *)) {
return [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation;
}
return [UIApplication sharedApplication].statusBarOrientation;
}
- 监听页面方向变化
在iOS13之前使用一下消息通知Key来注册监听,通过监听状态条的方向变化来监听页面方向变化
UIApplicationWillChangeStatusBarOrientationNotification
UIApplicationDidChangeStatusBarOrientationNotification
其他消息通知Key
UIApplicationStatusBarOrientationUserInfoKey
UIApplicationWillChangeStatusBarFrameNotification
UIApplicationDidChangeStatusBarFrameNotification
UIApplicationStatusBarFrameUserInfoKey
在iOS13以后以上的所有key虽然都已经DEPRECATED了,但仍然可以使用,但是苹果建议使用viewWillTransitionToSize:withTransitionCoordinator:
来替代页面方向变化监听。
- UIInterfaceOrientationMask页面方向
UIInterfaceOrientationMask
是iOS6之后增加的一种枚举,他是一个为了实现支持多种屏幕方向UIInterfaceOrientation
而定义的类型。这个枚举值也定义在UIApplication.h
中。
- 枚举类型
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),
}
转屏相关方法介绍
- AppDelegate中的相关方法
//iOS6在UIApplucationDelegate中提供这个方法,用于指定UIWindow的界面屏幕方向
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
return UIInterfaceOrientationMaskPortrait;
}
- 根视图中的转屏相关方法
常用方法:
方法1:设置决定当前页面是否可以自动旋转,YES为支持旋转,NO为不支持旋转;如果返回NO,则其他转屏相关方法将不会再被调用。
- (BOOL)shouldAutorotate
方法2:设置页面支持的旋转方向。
iPhone上默认返回UIInterfaceOrientationMaskAllButUpsideDown;
iPad上默认返回UIInterfaceOrientationMaskAll;
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
方法3:设置进入页面时默认显示的方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentatio
方法4:重写viewWillTransitionToSize: withTransitionCoordinator:方法来处理旋转后的事件(iOS8之后可以使用此方法,如需要适配iOS8以前的设备仍需要注册转屏监听)
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
if(size.width > size.height) {
//横屏处理
} else {
//竖屏处理
}
}
方法1、2、3为iOS6以后常用方法,方法4为iOS8以后监听处理转屏完成的方法;
**其他方法:**
//页面尝试旋转到与设备屏幕方向一致,当interface orientation和device orientation方向不一致时,希望通过重新指定 interface orientation 的值,立即实现二者一致
+ (void)attemptRotationToDeviceOrientation
//应用将要使用界面支持的方向,或者将要自动旋转 (在iOS6以后被禁用,要兼容iOS 6还是需要实现这个方法)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
//获取用户界面的方向 (方法在iOS8被禁用)
@property(nonatomic,readonly) UIInterfaceOrientation interfaceOrientation
//页面将要旋转(iOS8以后已经失效)
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
//页面已经旋转完成(iOS8以后已经失效)
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
//将要动画旋转到用户界面(iOS8以后已经失效)
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
//界面切换到一半时的回调(iOS5以后已经失效)
- (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
- (void)didAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
- (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration
注:再iOS8以后willRotateToInterfaceOrientation和willAnimateRotationToInterfaceOrientation的替代方法为viewWillTransitionToSize:withTransitionCoordinator
- attemptRotationToDeviceOrientation 使用说明
该方法的使用场景是 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,
需要先将 supportedInterfaceOrientations 的返回值改成Landscape,然后调用 attemptRotationToDeviceOrientation方法,系统会重新询问支持的 interface orientation,已达到立即更改当前 interface orientation的目的
- 系统版本区别
iOS 6 and above
iOS6以后再控制器中需要实现的方法
1、shouldAutorotate
2、supportedInterfaceOrientations
3、preferredInterfaceOrientationForPresentation
iOS5 and before
iOS5之前控制器中需要实现的方法
shouldAutorotateToInterfaceOrientation
决定页面方向的因素
- 屏幕旋转控制的优先级
Device Orientation控制配置 = AppDelegate中window配置 >根视图控制器配置 >最上层视图控制器配置
- 屏幕旋转控制设置
- Device Orientation方向配置:
在Xcode中依次打开:General→Deployment Info→Device Orientation 设置支持的旋转方向,下面的图片是默认设置。
注意:如果这四个选项都不选的话,Device Orientation 的值为默认值。在这里设置值与 info.plist中设置的值是同步的,也就是说修改其中的一个,另一个的值也会相应的变化。
- Info.plist中方向配置:
上面说了Info.plist中Info.plist中的Supported interface orientation的值与Device Orientation的值是同步的,所以在我们设置完Device Orientation再到info.plist中查看Supported interface orientation的值,发现是一样的
注意:在1和2中的设置都是全局控制的
- 在UIApplication中window支持方向设置:
iOS6的UIApplicationDelegate提供了下述方法,能够指定 UIWindow 中的界面的屏幕方向,该方法默认值为 Info.plist 中配置的 Supported interface orientations 项的值
//设置window只支持竖屏
- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
return UIInterfaceOrientationMaskPortrait;
}
window为AppDelegate中所持有的唯一的,并且是全局的,所以在方法中设置的屏幕方向也是全局有效的。
- 视图控制器中的方向配置:
在window的根视图rootViewControlle
r和viewController是通过modal模态弹出方式显现的时候页面旋转的相关方法才会被调用,当前controller及其所有childViewController都在此作用范围内。如果需求是想控制单个界面支持转屏,就需要在当前视图控制器中重写以下方法了
//Interface的方向是否会跟随设备方向自动旋转,如果返回NO
- (BOOL)shouldAutorotate {
return YES;
}
//返回直接支持的方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskPortrait;
}
//返回最优先显示的屏幕方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
这里涉及到的控制器有以下三个:
UITabbarViewController,UINavigationBarController ,UIViewController
当页面嵌套三种控制器使用时,其优先级为:
UITabbarViewController>UINavigationBarController >UIViewController
说明:
* 如果是单一的`viewController,`其转屏方向取决于此控制器的转屏方法中的配置;
* 如果是`UINavigationBarController + UIViewController`,其转屏方向取决于`UINavigationBarController`导航控制器的转屏方法配置;
如果是UITabbarViewController + UINavigationBarController + UIViewController
其转屏方向取决于UIViewController
导航控制器的转屏方法配置;
总结:
* 如果`viewController`存在根视图控制器,则`viewController`的转屏相关方法就不会再被调用,在`UINavigationBarController + UIViewController`结构下`UINavigationBarController`的方法会被调用,`UIViewController`的方法失效;在`UITabbarViewController + UINavigationBarController + UIViewController`结构下`UITabbarViewController`的方法会被调用,其他两个的方法失效。所以如果期望使用当前`viewController`控制器来决定是否转屏就会产生问题(下面会说怎样由具体VC来控制),因为这个方法被根视图控制器拦截了!
- 如何决定屏幕最终支持的方向
决定界面最后支持的屏幕方向的是 target&plist ∩ AppDeleagte中window设置 ∩ 视图控制器设置 这三个位置的交集。如果这个交集为空,就会抛出UIApplicationInvalidInterfaceOrientation
异常崩溃。
如何使用具体VC来控制
可以创建UITabbarViewController 、 UINavigationBarController 、 UIViewController 的子类或分类,在子类或分类中重写以下代码
- 只存在单独的viewController时如何设置
- (BOOL)shouldAutorotate{
return NO;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAll;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return UIInterfaceOrientationPortrait;
}
- 控制器结构为UINavigationController+UIViewController时如何设置
需要在导航控制器UINavigationController下设置
//返回导航控制器的顶层视图控制器的自动旋转属性,因为导航控制器是以栈的原因叠加VC的
-(BOOL)shouldAutorotate{
return self.topViewController.shouldAutorotate;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return self.topViewController.supportedInterfaceOrientations;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return self.topViewController.preferredInterfaceOrientationForPresentation;
}
- 控制器结构为UITabbarViewController+UINavigationController+UIViewController时如何设置
需要在UITabbarViewController中设置
-(BOOL)shouldAutorotate
{
return self.selectedViewController.shouldAutorotate;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return self.selectedViewController.supportedInterfaceOrientations;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return self.selectedViewController.preferredInterfaceOrientationForPresentation;
}
- 使用模态视图
使用模态modal弹出的viewController不在受到根视图的控制,具体的设置和普通视图器代码相同。
强制转屏
- 方法一
私有方法,无法直接调用
[[UIDevice currentDevice] setOrientation:UIInterfaceOrientationPortrait];
可以间接调用,上线未被拒
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationUnknown] forKey:@"orientation"];
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationPortrait] forKey:@"orientation"];
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationUnknown] forKey:@"orientation"];
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationLandscapeLeft] forKey:@"orientation"];
- 方法二
也是调用私有方法,
- (void)setScreenOrientation:(UIInterfaceOrientation)orientation
{
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 = orientation;
[invocation setArgument:&val atIndex:2];
[invocation invoke];
}
}
- 方法三
旋转view的transform
可以通过旋转view的transform属性达到强制旋转屏幕方向的目的,但是这样会有很多问题,以及适配问题,AlertView方向,状态条方向等。
//设置statusBar
[[UIApplication sharedApplication] setStatusBarOrientation:orientation];
//计算旋转角度
float arch;
if (orientation == UIInterfaceOrientationLandscapeLeft)
arch = -M_PI_2;
else if (orientation == UIInterfaceOrientationLandscapeRight)
arch = M_PI_2;
else
arch = 0;
//对根视图控制器进行强制旋转
self.navigationController.view.transform = CGAffineTransformMakeRotation(arch);
self.navigationController.view.bounds = UIInterfaceOrientationIsLandscape(orientation) ? CGRectMake(0, 0, SCREEN_HEIGHT, SCREEN_WIDTH) : initialBounds
注意:
- [[UIApplication sharedApplication] setStatusBarOrientation这个方法在iOS9以后已经失效,使用会有警告。如果将shouldAutorotate设置为YES,setStatusBarOrientation方法设置无效,只有shouldAutorotate设置为NO,才会起作用。
开发中的问题
- 在系统锁屏按钮开启,播放器横屏播放,进入后台,再回前台横屏变竖屏,需求仍是横屏
解决:在App从后台返回前台后EnterForeground时判断当前页面方向,如果是横屏将页面强制横屏。
- 在6p,7p一些支持桌面横屏的设备上,横屏启动App页面横屏显示,需求是启动竖屏
解决:在targe的Device Orientation中只设置portrait一项
- info.plist中设置Initial interface orientation未起作用
解决:原因未找到