iOS横竖屏切换

基础概念

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关于横竖屏的三个方法:

  1. shouldAutorotate,页面是否允许自动旋转,被弃用api:-shouldAutorotateToInterfaceOrientation的取代者;默认值为YES,表示当前界面允许跟随设备旋转而自动旋转;
  2. supportedInterfaceOrientations,该界面支持的界面朝向,可以返回四个朝向的任意组合,iPad默认值是四个朝向都支持,iPhone默认值是除了UpsideDown的三个朝向。这个方法回调的前提是shouldAutorotate=YES。
  3. 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 )coordinator API_AVAILABLE(ios(8.0));

你可能感兴趣的:(iOS横竖屏切换)