关于横竖屏适配,坑挺深的。只要明白以下几点,就可以避免几个大坑, 具体UI上的坑就是因项目而异了.

1.横竖屏方向枚举

关于横竖屏一共有三种枚举,UIInterfaceOrientation,UIInterfaceOrientationMask,UIDeviceOrientation。

1.1 UIInterfaceOrientation与UIDeviceOrientation

为什么这两个放在一起说,好吧,你看看下面这个枚举定义:

typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
    UIInterfaceOrientationUnknown            = UIDeviceOrientationUnknown,
    UIInterfaceOrientationPortrait           = UIDeviceOrientationPortrait,
    UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
    UIInterfaceOrientationLandscapeLeft      = UIDeviceOrientationLandscapeRight,
    UIInterfaceOrientationLandscapeRight     = UIDeviceOrientationLandscapeLeft
}

我相信你应该看出了点东西了把,对于iOS设备来讲,屏幕状态由以上五种状态。上下翻转还是很好区分的,左右旋转可能就不是很好区分。

这里有个坑!!!
请仔细观察上面的枚举值。

在处于竖屏和上下翻转的状态下这两个枚举值是一样的,而当处于横屏时,这两个值刚好相反。

所以在有时你发现跟你预期的翻转方向不一样的时候,可能你用错了枚举。

UIDeviceOrientation 是设备的当前所处的方向,而且事实上它有6个值,

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
}

分别对应iPhone未知方向,竖直,上下反转,向左旋转,向右旋转,屏幕朝上,屏幕朝下。关于横屏如何去分左右,其实API中的注释已经说明,当处于UIDeviceOrientationLandscapeLeft,home键在右侧,当处于UIDeviceOrientationLandscapeRight,home键在左侧。

所以,UIDevice顾名思义,事实上是用来判断设备方向的。

UIInterfaceOrientation 即当前页面的方向。

在设备进行横屏旋转的时候,为了横屏时上下不翻转,所以当Device处于Left时,界面应该是Right旋转。这样才不会使横屏时内容上下翻转。所以我想你应该明白了为什么在处于横屏时为什么他们俩的值是刚好相反的。

所以对于横竖屏适配,使用的枚举大家一定要看好,使用UIInterfaceOrientation。不要搞反。

1.2 UIInterfaceOrientationMask

其实苹果还是给了我们更清晰和方便的枚举如下:

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),
}

事实上我们在横竖屏适配时,最常用的是这个枚举。这个枚举详细的列举了各种你需要的情况。我就不赘述了。官方的命名还是很舒服很好理解的。

2.开启横竖屏权限

开启横竖屏的方式有两种,

一种是在项目中直接进行勾选: 

 可以看到这种勾选方式允许你进行四个方向的配置,并且这种勾选方式会直接在你的项目plist文件中添加对应的key-value, 在plist中可以更精细的设置pad和手机, 比如手机上只支持竖屏, 而pad上支持全方向.

但是由于在plist这里配置是对项目启动时lanuch界面产生影响,比如设置了手机支持全方向, 当用户以横屏的方式启动app时, 会发现lanuch界面也会横屏展示,而往往我们又没有对lanuch进行横竖屏适配,案例:  微信,淘宝在横屏启动时, launch界面也是横屏的.

所以在这个时候我们就需要使用第二种方式进行配置。

在项目中的AppDelegate文件中通过代码进行配置, 这样的配置更加灵活, 能通过pad, 手机做不同的逻辑,  此处还可以作为app能否支持旋转的总开关,

// 横屏启动app, 会把启动图也变成横屏的样子, 通过此处控制方向,可以保证启动图是竖向的
- (UIInterfaceOrientationMask )application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    NSLog(@"%s",__func__);
    if (is_pad) {
        return UIInterfaceOrientationMaskAll;
    }
    return UIInterfaceOrientationMaskPortrait;
}

搭配UIInterfaceOrientationMask使用,你可以很方便的让你项目开启你所需要的横竖屏权限和限制条件。

大致说下原理 : app在启动的过程中, 系统只会读取plist中的配置,  APPdelegate中的代理方法并不会执行.

  • 如果在plist中设置了支持横屏, 而当前也是横屏, 系统就会已横屏的方式加载启动图了.
  • 如果在plist中没有设置或者设置了只支持竖屏, 那么系统就会以竖屏的方式加载启动图,可以保证启动图一定是竖屏加载的.
    而后续当触发重力感应变化时, APPdelegate中的代理方法就会被调用, 横竖屏转化就可以正常执行, 

综上, 更推荐使用APPdelegate中的代理方法进行横竖屏的配置.

3.在VC中如何控制横竖屏    

在我们开启了项目的横竖屏的限制之后,需要在ViewController进行相应的配置,才能真正实现横竖屏。

开启横竖屏,我们需要在VC中添加如下代码:

// 设备支持方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskAll;
}
// persent出页面时的默认方向, 
// 网上很多文章认为是进入页面时的默认方向, 这个是不对的, 这个方法只针对present出来的vc, 注意看方法名后面  ForPresentation
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationPortrait; 
}

而对于横竖屏,手机端一般有两种情况,一种是手机没有开启横竖屏锁定,用户将手机横屏时触发的。对于第一种情况,我们只需要在VC中添加:

// 当前页面是否支持自动转屏
- (BOOL)shouldAutorotate {
    return YES;
}

另一种是我们在项目中的某些条件下强行让屏幕横屏,例如大图预览,视频播放,等等。而对于这种情况,我们可以使用下面👇这两种方法,都可以实现效果:

- (void)setInterfaceOrientation:(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];
    }
}

+ (void)rotateToDirection:(UIDeviceOrientation)orientation {
    UIDevice *device = [UIDevice currentDevice];
    // 如果当前方向和目的方向一致,会发生转不过去,
    // 此时先设置Unknown,在设置目标方向即可
    if (device.orientation == orientation) {
        NSNumber *value = @(UIDeviceOrientationUnknown);
        [[UIDevice currentDevice] setValue:value forKey:@"orientation"];
    }
    NSNumber *value = @(orientation);
    [device setValue:value forKey:@"orientation"];
}

PS:这两个方法只有在- (BOOL)shouldAutorotate( return YES; )时,才会生效。并且请注意使用的枚举值的不同。

4.横竖屏控制优先级

在我们接手一个项目后,说要添加一个某个界面横竖屏需求时,发现按照上面的方式配置了一圈,发现还是转!不!成!功!What F***!!!

事实上在这里我们要了解一个问题,就是关于横竖屏控制的优先级。对于限于VC范围来讲优先级最高的是APPdelegate中的代理方法,然后是windowrootViewController,而往往我们的项目结构是容器视图控制器控制VCtabBarController控制navigationController之后是VC,而横竖屏控制的优先级也是跟你的项目架构一样。而且是一旦优先级高的关闭了横竖屏配置,优先级低的无论如何配置都无法做到横竖屏。所以在你接受这个需求的时候,你需要看一下根视图的配置。

对于这种情况,我们有两种处理方式,一种是通过模态的方式跳转的下个VC,这个VC是隔离出来的,不在你之前的容器里,不会受到rootViewController的影响。

而另一种我们需要改造一下根视图的配置:

// tabbar中方法
-(BOOL)shouldAutorotate {
    return [self.selectedViewController shouldAutorotate];
}

-(UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return [self.selectedViewController supportedInterfaceOrientations];
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return [self.selectedViewController preferredInterfaceOrientationForPresentation];
}


// 导航栏中方法
-(BOOL)shouldAutorotate {  
    return [[self.viewControllers lastObject] shouldAutorotate];  
}  

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {  
     return [[self.viewControllers lastObject] supportedInterfaceOrientations];  
}  

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {  
    return [[self.viewControllers lastObject] preferredInterfaceOrientationForPresentation];  
}

可以看到我们通过获取push栈中的最后一个VC的属性来进行设置。

总之要知道优先级的问题,general == appDelegate >> rootViewController >> nomalViewController

明白了权限的优先级以及开启的方法我想转屏就很显而易见了。

5.横竖屏适配

事实上旋转屏幕成功,对于iOS横竖屏问题我们只是完成了一半。另一半就是UI适配问题,其实这个要说起来就比较麻烦了,有些时候有很多case需要针对对应的业务条件来定制。但是无外乎几种实现思路。这里博主给大家抛几块砖哈:

首先我们要知道,当发生转屏事件时,系统的回调方法是:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    // 即将开始转屏
    [self viewWillBeginTransitionWithSize:size];
    WS(weakSelf)
    [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
        [weakSelf viewDidEndTransitionWithSize:size];
    }];
}
/// 子视图即将开始旋转
- (void)viewWillBeginTransitionWithSize:(CGSize)size {
    if (size.width > size.height) { // 横屏
        // 横屏布局 balabala
    } else {
        // 竖屏布局 balabala
    }
}
/// 子视图旋转完成
- (void)viewDidEndTransitionWithSize:(CGSize)size {
    if (size.width > size.height) { // 横屏
        // 横屏布局 balabala
    } else {
        // 竖屏布局 balabala
    }
}

首推的方式是使用约束布局,在使用约束布局时,横竖屏转换时,在通常情况下约束条件会很相似,所以在布局上会极大的减少代码量。其次如果有个别的特殊问题,可以在上面的回调方法里面进行微调。

其次,对于转屏后,[UIScreen mainScreen].bounds.size以及self.view.frame.size的宽高系统会自动调换。即在横屏的时候width > height。所以在我们进行常规布局的时候我们可以选择控件的frame属性都与这两个属性进行比例换算。这样在当横竖屏转换的时候,重布局时,也会适应成对应屏幕下的布局。同样有需要特殊处理的布局,在上面的回调方法中进行细节微调即可。

  • 子视图即将开始旋转
    - (void)viewWillBeginTransitionWithSize:(CGSize)size
  • 子视图旋转完成
    - (void)viewDidEndTransitionWithSize:(CGSize)size

对于子视图,在横竖屏切换时,还会触发子视图重布局的方法, 但是layoutSubviews 会在view的生命周期内多次触发, 如果实在万不得以可以写在这里设置约束:

- (void)layoutSubviews {
    [super layoutSubviews];
    // 通过状态栏电池图标来判断屏幕方向
    if ([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationMaskPortrait) {
        // 竖屏 balabala
    } else {
        // 横屏 balabala
    }
}

当然我只是说了几种比较简单的处理方式,和应对方法,对于整个项目都需要横竖屏适配的,我想还是一个比较复杂的过程。在实在处理不了的问题上,也可以通过写两套布局的方式来处理。至于过场动画,理论上如果你用约束和我说的比例布局的方式来写,基本系统会自动帮你这个问题给处理掉。但如果两种布局差距很大,你用了两套完全不同的布局,那这个你可能就要伤脑筋了。哈哈哈。不过有一些情况处理要求不严格的话可以使用截图过场大法来解决。

6.界面跳转

当前页面横竖屏适配已经完整了, 在横屏下有个按钮, 点击push到一个新的页面, 但是新页面只支持竖屏, 此时点击会发现, 本来是一个只支持竖屏的页面也变成了横屏展示.这可真是有点头大.

我们总共需要处理以下4种场景: 

  • 竖屏  push 到竖屏
  • 竖屏  push 到横屏
  • 横屏 push 到横屏
  • 横屏 push 到竖屏

竖屏 到 竖屏, 和横屏到横屏, 都是比较简单的, 什么都不需要做, 系统就可以帮我们完成.  竖屏 到 横屏, 和 横屏到竖屏 需要额外做一些工作.

解法1: 

点击按钮的时候, 现在当前页面转到竖屏, 然后在调用push方法.

好处是能解决问题, 影响范围比较小,但是不是一个全局的通用方法, 只能在各个页面进行调用, 适用于大部分页面是竖屏, 只有少量页面是特殊处理的场景.

解法2: 

重写nav的push/pop方法, 在push/pop方法中, 获取下下一级页面支持的方法, 如果当前方向包含在下级页面支持的方法中, 直接push;  如果不包含, 调用强制转屏的方法转到对应的方向.

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {

    // 拿到当前的方向, 在获取下一个页面支持的方向,
    // 下一级页面支持的方向中包含当前方向,直接push,
    // 下一级页面支持的方向不包含当前方向,转到对应的方向在push

    NSLog(@"当前方向  %zd",[UIApplication sharedApplication].statusBarOrientation);
    NSLog(@"下级页面方向  %zd",[viewController supportedInterfaceOrientations]);

    BOOL contain = [viewController supportedInterfaceOrientations] & (1<<[UIApplication sharedApplication].statusBarOrientation) ;
    NSLog(@"计算结果  %d",contain);

    if (contain) {
        [super pushViewController:viewController animated:animated];
    } else {
        // 只有第一次push的时候管用, 后续的都不管用了
        [self p_changeToTargerOrientations:viewController];
        [super pushViewController:viewController animated:animated];
    }
}

- (void)p_changeToTargerOrientations:(UIViewController *)viewController {

    if ([viewController supportedInterfaceOrientations] & (1<<UIInterfaceOrientationPortrait)) {
        [AppDelegate interfaceOrientation:UIInterfaceOrientationPortrait];
    } else if ([viewController supportedInterfaceOrientations] & (1<<UIInterfaceOrientationLandscapeLeft)) {
        [AppDelegate interfaceOrientation:UIInterfaceOrientationLandscapeLeft];
    } else if ([viewController supportedInterfaceOrientations] & (1<<UIInterfaceOrientationLandscapeRight)) {
        [AppDelegate interfaceOrientation:UIInterfaceOrientationLandscapeRight];
    } else if ([viewController supportedInterfaceOrientations] & (1<<UIInterfaceOrientationPortraitUpsideDown)) {
        [AppDelegate interfaceOrientation:UIInterfaceOrientationPortraitUpsideDown];
    } else {
        [AppDelegate interfaceOrientation:UIInterfaceOrientationPortrait];
    }
}

好处是比较通用, 坏处是影响范围太大, 难以控制.

解法3: 

项目中的vc都有一个基类, 重写基类的生命周期方法, 在生命周期方法中调用强制转屏的方法.个人理解是只需要在- viewWillAppear调用一次就够了, 但是场景复杂之后, 发现只在一处写是不够的, 只能在- viewDidAppear在补一次, 这样的补丁代码, 写着确实有点难受.

  • - (void)viewWillAppear:(BOOL)animated 
  • - (void)viewDidAppear:(BOOL)animated
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [AppDelegate interfaceOrientation:UIInterfaceOrientationLandscapeRight];
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [AppDelegate interfaceOrientation:UIInterfaceOrientationLandscapeRight];
}

这个影响范围也比较大.

实际在项目中, 解法2,3需要大量的场景去测试, 在做的时候发现了一些问题, 没有把握在发版前全部处理完,而且不产生新的问题, 所以最终采用了解法1, 但是把解法2,3的思路记录下, 后续还是期望往解法2,3上做.

7.总结

iOS横竖屏适配,确实有很多坑,当然,有些坑是系统的;而有些坑,是因为我们的无知而造成的。所以多看多学多做多理解,必然能让你学到更多,增强填坑的硬实力。而对于横竖屏适配这块,转屏并不难,难的是横竖屏布局适配。博主只是简单的说了一些思路,至于实现起来,还是要针对对应的需求来进行处理。下面👇给大家链接几个博主觉得还不错的关于适配的文章,大家可以看看:

关于iOS横竖屏适配 - 简书

iPad横竖屏下的代码适配

iOS 屏幕适配,autoResizing autoLayout和sizeClass图文详解

博主私人博客@HarwordLiu

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐