基础概念
UIDeviceOrientation
UIDeviceOrientation,表示设备朝向,可以通过[UIDevice currentDevice] orientation]
获取,取值有:
typedef NS_ENUM(NSInteger, UIDeviceOrientation) {
UIDeviceOrientationUnknown, // 未知,启动时会出现
UIDeviceOrientationPortrait, // 竖屏,home键在底部
UIDeviceOrientationPortraitUpsideDown, // 倒立,home键在顶部
UIDeviceOrientationLandscapeLeft, // 左横屏,home键在右边
UIDeviceOrientationLandscapeRight, // 右横屏,home键在左边
UIDeviceOrientationFaceUp, // 屏幕朝上
UIDeviceOrientationFaceDown // 屏幕朝下
}
UIInterfaceOrientation
UIInterfaceOrientation,表示页面内容朝向,注意UIInterfaceOrientation和UIDeviceOrientation的关系,其中UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,这是因为:
This is because rotating the device to the left requires rotating the content to the right.
不用特别细究两者之间关系,我们只需要根据需要设置好UIInterfaceOrientation即可,通过
[UIApplication shareApplication] statusBarOrientation]
可以获取当前状态栏朝向。
typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
UIInterfaceOrientationUnknown = UIDeviceOrientationUnknown,
UIInterfaceOrientationPortrait = UIDeviceOrientationPortrait,
UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
};
UIInterfaceOrientationMask
UIInterfaceOrientationMask,是由页面内容朝向的二进制偏移组成,用来更方便描述某个界面支持的朝向。比如说下面的UIInterfaceOrientationMaskLandscape,其实就是由MaskLandscapeLeft和MaskLandscapeRight组成,这样可以方便描述设备支持两个横屏方向。
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),
};
UIViewController相关
UIViewController关于横竖屏的三个方法:
- shouldAutorotate,页面是否允许自动旋转,被弃用api:
-shouldAutorotateToInterfaceOrientation
的取代者;默认值为YES,表示当前界面允许跟随设备旋转而自动旋转; - supportedInterfaceOrientations,该界面支持的界面朝向,可以返回四个朝向的任意组合,iPad默认值是四个朝向都支持,iPhone默认值是除了UpsideDown的三个朝向。这个方法回调的前提是shouldAutorotate=YES。
- preferredInterfaceOrientationForPresentation,该界面被present出来的界面朝向,可以返回四个朝向的任意组合。如果没有返回,则present时和原来界面保持一致。
AppDelegate相关
AppDelegate的supportedInterfaceOrientationsForWindow方法,根据需要返回当前window是否支持横屏。
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window;
工程配置相关
在xcode的工程设置的General可以配置iPhone和iPad的页面朝向支持。
横竖屏切换实例
竖屏界面如何present横屏界面
竖屏present横屏是很普遍的场景,比如说视频播放场景的全屏切换,就可以在当前竖屏的界面present一个横屏播放界面的方式,实现横竖屏切换。具体的操作步骤只需要两步:
1,设置modalPresentationStyle为UIModalPresentationFullScreen;
2、preferredInterfaceOrientationForPresentation方法,返回UIInterfaceOrientationLandscapeRight;
比如说下面的LandscapeViewController界面:
// 点击时设置
- (void)onClick {
LandscapeViewController *landVC = [[LandscapeViewController alloc] init];
landVC.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:landVC animated:YES completion:nil];
}
// LandscapeViewController内部代码
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationLandscapeRight;
}
思考🤔:
1、如果是横屏转竖屏呢?
2、如果想要自定义旋转效果实现呢?(UIViewControllerAnimatedTransitioning协议)
竖屏界面如何push横屏界面
比如说这样的场景:App的rootVC是navigationVC,导航栈内先有一个竖屏界面,现在想要push一个横屏界面LandscapeViewController。
一个简单的方式如下:
// appdelegate实现
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
if ([self.navigationVC.topViewController isKindOfClass:LandscapeViewController.class]) {
return UIInterfaceOrientationMaskLandscapeRight;
}
else {
return UIInterfaceOrientationMaskPortrait;
}
}
// LandscapeViewController内部实现
- (void)viewDidLoad {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:@selector(setOrientation:)]];
invocation.selector = NSSelectorFromString(@"setOrientation:");
invocation.target = [UIDevice currentDevice];
int initOrientation = UIDeviceOrientationLandscapeRight;
[invocation setArgument:&initOrientation atIndex:2];
[invocation invoke];
}
思考🤔:
1、这里为什么没有用到UIViewController的三个方法?
2、在viewDidLoad调用的旋转方法是什么意思?
横屏竖切换机制分析
前面的实例介绍了如何支持切换,但是也产生一些疑问:
工程配置文件也没有设置横屏,为什么后面就能支持横屏?
工程配置、AppDelegate、UIViewController这三者,在横竖屏切换过程的关系是什么?
自动旋转和手动旋转有什么区别?
....
仅仅知道切换适配代码,是无法形成横竖屏切换理解,也就很难回答上述的问题。
由于没有找到解释横竖屏切换机制的官方文档,以下根据自己的经验对这个切换的机制进行分析。
系统如何知道App对界面朝向的支持
这里分两种情况,App启动前和App运行时。
App启动前
在App启动前进程还未加载,代码无法运行,系统肯定无法通过AppDelegate或者UIViewController这种代码的方式获取横竖屏的配置。所以在这种情况下,工程配置中的plist描述App对屏幕的适配,就可以很好帮助系统识别应该以什么样的朝向启动App。
所以在plist中增加横屏的支持,好处是开屏能够支持横屏,这样界面展示更加顺滑;坏处也是开屏支持了横屏,导致开屏为横屏启动的时候,UIScreen的mainScreen是横屏的大小,但很多业务逻辑代码都会以[UIScreen mainScreen]去取屏幕宽度和高度,所以很容易取到错误的值。
App运行时
当App进程加载完成,此时系统可以通过运行时询问的方式,来动态获取不同时机的界面朝向。
此时AppDelegate控制的是UIWindow层面的朝向,UIViewController控制的是VC层面的朝向。需要注意的是,当我们返回UIViewController的朝向时,还要考虑父容器的朝向。通常一个App的界面层级是UIWindow=>RootViewController(容器vc)=>UIViewController(界面vc)。假如只在UIWindow返回界面朝向也是允许的,就如同上面的实例分析中的push横屏。
不同界面的朝向控制
还是假设UIWindow=>RootViewController(容器vc)=>UIViewController(界面vc)的层级,且当前ViewController是竖屏vc,现在需要push一个横屏界面LandscapeViewController。
在每次界面切换的时候,系统都会回调确认新的界面朝向,最终结果为UIWindow朝向、容器vc朝向、界面vc朝向三者的“与”值。那么假如这个值冲突了呢?
假如supportedInterfaceOrientationsForWindow一直返回的竖屏,那么后面VC设置横屏不会生效;
类似,假如UIWindow设置的是横屏,那么后面VC设置竖屏也不会生效;
如果在界面切换的过程中发现返回的朝向值未确定,系统更倾向于保持当前朝向不变,并且可能会遇到以下的crash。
Terminating app due to uncaught exception 'UIApplicationInvalidInterfaceOrientation', reason: 'Supported orientations has no common orientation with the application, and [LandscapeViewController shouldAutorotate] is returning YES'
这个原则同样适用于当返回多个结果,比如说当前界面是竖屏,然后UIWindow和ViewController的界面朝向都支持横屏和竖屏都支持,那么此时会保持竖屏。
一种比较常用的设计:
// AppDelegate
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
return self.navigationVC.topViewController.supportedInterfaceOrientations;
}
// NavigationController
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return self.topViewController.supportedInterfaceOrientations;
}
// LandscapeViewController
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationLandscapeRight;
}
自动旋转和手动旋转
自动旋转指的是我们旋转物理设备时,系统会触发界面的旋转。当我们从一个竖屏界面push一个横屏界面时,即使横屏界面设置了shouldAutorotate=YES,这个界面也不会变成横屏,但是拿起来设备左右翻转的时候,会发现随着设备旋转,界面也从横屏变成了竖屏。这就是自动旋转。
手动旋转指的是手动触发旋转,根据经验发现有两个api,UIViewController的+attemptRotationToDeviceOrientation
,还有UIDevice的setOrientation:
可以调整界面朝向。前者是将界面朝向对齐设备朝向,是标准api;后者是调整设备朝向,是私有api。
假如我们在很多个竖屏界面中,需要强制横屏某一个界面,如果是子界面可以使用present的方式,如果是push那么就必须要用到这个私有api。
注意事项
其他横竖屏适配方式
1、视图适配:通过transform修改layer从而在视图上实现横屏,但是此时屏幕宽度、状态栏、安全距离等都保留竖屏状态,这种方式仅仅适用于横屏弹窗等部分场景;
2、新建Window:由于App的适配是UIWindow为单位,那么理论上是可以新建一个UIWindow来横屏的界面;
横竖屏切换通知
NSNotification通知
[[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceOrientationDidChangeNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"NSNotification:%@, orientation:%d", note.userInfo, [(UIDevice *)note.object orientation]);
}];
UIViewController回调
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator API_AVAILABLE(ios(8.0));