iOS屏幕旋转解决方案

6,125 阅读7分钟
1.导航控制器栈内部的VC方向是导航控制器来决定的。nav --- A --- B --- C,C的旋转方法是不起作用的,靠的是nav的-(BOOL)shouldAutorotate-(UIInterfaceOrientationMask)supportedInterfaceOrientations

解决方案是:重写nav的旋转方法,把结果指向到topViewController:

-(BOOL)shouldAutorotate{
   return self.topViewController.shouldAutorotate;
}

-(UIInterfaceOrientationMask)supportedInterfaceOrientations{
    return self.topViewController.supportedInterfaceOrientations;
}

对于UITabBarController,就转嫁为它的selectedViewController的结果。

2.旋转的逻辑流是:手机方向改变了 ---> 通知APP ---> 调用APP内部的关键VC(TabBar或Nav)的旋转方法 ---> 得到可旋转并且支持当前设备方向 ---> 旋转到指定方向。

逻辑流的初始时物理上手机方向改变了。所有如果A push到 B,A只支持竖屏,而B只支持横屏,如果这时手机物理方向没变,那么B还是会跟A一样竖屏,哪怕它只支持横屏并且问题1也解决了的。

解决方案:强制旋转。

@implementation UIDevice (changeOrientation)

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


@end

UIDevice提供一个category,调用setOrientation:这个私有方法来实现。

3.有了问题1和2的解决,对于整个项目的基本方案确定。一般项目会有一个主方向,绝大多数界面都是这个方向,比如竖屏,然后有特定界面是特定方向。

那么解决方案是:

  • 在target --> General --> Development Info里配置支持所有可能的方向
  • 使用baseViewController,项目所有VC都继承与它,在baseVC里写入默认方向设置,这个默认设置就是绝大多数界面支持的方向。
  • 然后在特殊方向界面,重写-(BOOL)shouldAutorotate-(UIInterfaceOrientationMask)supportedInterfaceOrientations来达到自己的目的。
  • 特殊界面因为要强制旋转,所以在进入界面是旋转到需要方向:
  -(void)viewWillAppear:(BOOL)animated{
   [UIDevice changeInterfaceOrientationTo:(UIInterfaceOrientationLandscapeLeft)];
}
4.push和pop的结果测试:

要验证问题3的方案是否满足需要,满足需要的意思是:每个页面能够显示它支持的方向而且不会干扰到其他界面。所以测试一下push和pop的情况。

测试变量有:

  • 当前的界面是默认还是特殊,这里把只有竖屏设为默认,横屏为特殊情况。默认代表这个VC只需继承baseVC的方向相关方法,不做任何额外处理。
  • 界面是push还是pop
  • 下一个界面是默认情况还是特殊情况。
  • 下一个界面的shouldAutorate是否为YES。

前后两个界面方向一致的画,结果肯定是好的,就不测试 了。最终测试结果如下:

动作 当前 目标 目标可旋转 结果
push 默认 特殊 ✔️ 成功
push 默认 特殊 失败
push 特殊 默认 ✔️ 失败(2)
push 特殊 默认 失败(3)
pop 默认 特殊 ✔️ 成功
pop 默认 特殊 失败
pop 特殊 默认 ✔️ 成功(1)
pop 特殊 默认 成功(1)
  • 成功代表目标界面旋转到了期望的方向
  • 标记1:没有旋转,直接显示的默认样式(竖屏)。
  • 标记2:从横屏到竖屏,没有切换方向,为什么?因为UIDevice的方向没有修改,没有触发切换效果。所以在特殊界面离开的时候还要调用强制旋转。其实只要相邻的方向不同,就要在切换时触发强制旋转。
  • 添加了viewWillDisappear里的强制旋转后,标记2可以解决。但标记3还是失败,其实push时,下一个界面如果是不可旋转的,那么方向一定是不变了。

特殊界面只要保持,进入和离开时都调用强制旋转,并且自身shouldAutorate为YES,那么push或pop进入特殊界面都没有问题。关键是从特殊界面离开进入默认界面,pop时是成功的,push时如果默认界面是不可旋转的,就会失败。

针对这个有两种方案:

  • 在离开前把当前界面旋转为默认,先旋转,再push。
  • 把默认界面改为可旋转。
5.特殊方向界面离开前先旋转到默认

因为特殊界面支持的方向不包含默认方向,所以只是强制旋转时不起作用的,在强制旋转前还要修改支持的方向。具体代码:

- (IBAction)push:(id)sender {
   
   [self changeOrientationBeforeDisappear];  //离开前先修改方向,其他每个出口都要调用这个方法。不能在`viewWillDisappear`里调用,因为这时push等已经触发了
   TFThirdViewController *thirdVC = [[TFThirdViewController alloc] init];
   [self.navigationController pushViewController:thirdVC animated:YES];
}

-(void)changeOrientationBeforeDisappear{
   _orientation = UIInterfaceOrientationMaskPortrait;  //替换为默认方向
   [UIDevice changeInterfaceOrientationTo:(UIInterfaceOrientationPortrait)];
   _orientation = UIInterfaceOrientationMaskLandscapeLeft; //替换为特殊方向界面自身需要的方向
}

-(UIInterfaceOrientationMask)supportedInterfaceOrientations{
   return _orientation;  //根据变量变化而变化
}

如果下一个界面不是默认,会是什么情况?会有两次旋转。离开时旋转到默认,进入下一个界面,它自身又旋转到指定方向。效果不好,如果想一次到位,怎么办?就要离开的时候知道下一个界面期望的方向是什么,然后preferredInterfaceOrientationForPresentation正好符合这个意图。 所以修改为:

@interface TFSecondViewController (){
   UIInterfaceOrientationMask _orientation;
   UIInterfaceOrientationMask _needOrientation;
}

@end

@implementation TFSecondViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   
   _needOrientation = UIInterfaceOrientationMaskLandscapeLeft;
   _orientation = _needOrientation;
}

-(void)viewWillAppear:(BOOL)animated{
   [UIDevice changeInterfaceOrientationTo:(UIInterfaceOrientationLandscapeLeft)];
}

-(BOOL)shouldAutorotate{
   return YES;
}

- (IBAction)push:(id)sender {
   
   TFThirdViewController *thirdVC = [[TFThirdViewController alloc] init];
   [self changeOrientationBeforeDisappearTo:thirdVC];  //离开前先修改方向,其他每个出口都要调用这个方法。不能在`viewWillDisappear`里调用,因为这时push等已经触发了
   
   [self.navigationController pushViewController:thirdVC animated:YES];
}

-(void)changeOrientationBeforeDisappearTo:(UIViewController *)nextVC{
   _orientation = UIInterfaceOrientationMaskAll;  //改为任意方向
   [UIDevice changeInterfaceOrientationTo:[nextVC preferredInterfaceOrientationForPresentation]];
   _orientation = _needOrientation; //替换为特殊方向界面自身需要的方向
}

-(UIInterfaceOrientationMask)supportedInterfaceOrientations{
   return _orientation;  //根据变量变化而变化
}

@end

_needOrientation时当前页面需要的样式。

总结起来就是:

  • 给绝大多数情况建一个baseVC,里面设置默认方向。
  • 对特殊方向界面:
    • 进入时(viewWillAppear)强制旋转到需要的方向
    • 离开时,注意并不是viewWillDisappear,而是push操作之前,先修改方向为下一个界面的期望方向。
    • 当然自身的shouldAutorotate保持为YES。
  • 方向相关的3个方法全部要实现。因为基类(BaseVC)做了处理,可以省去绝大部分的工作。特殊方向的界面单个处理即可。
  • preferredInterfaceOrientationForPresentation的方向要和进入时的方向一致,这样就不会有2次旋转。

相比把基类的shouldAutorotate改为YES,这个方案的好处是,把特殊情况的处理基本都压缩在特殊界面自身内部了,依赖的只有其他界面的supportedInterfaceOrientations,这个方法是一个补充性的,不会干扰其他界面原本的设计。而对shouldAutorotate却比较麻烦,因为其他界面可能不希望旋转。

再次测试pop和push情况:

动作 当前 目标 目标可旋转 结果
push 默认 特殊 ✔️ 成功
push 特殊 默认 ✔️ 成功
push 特殊 默认 成功
pop 默认 特殊 ✔️ 成功
pop 特殊 默认 ✔️ 成功
pop 特殊 默认 成功
push 特殊1 特殊2 ✔️ 成功
pop 特殊1 默认2 ✔️ 成功
  • 特殊的都是可旋转的,所以这种情况剔除了
6.present和dismiss的情况
动作 当前 目标 目标可旋转 结果
present 默认 特殊 ✔️ 奔溃(1)
present 特殊 默认 ✔️ 成功
present 特殊 默认 成功
dismiss 默认 特殊 ✔️ 成功
dismiss 特殊 默认 ✔️ 成功
dismiss 特殊 默认 成功
present 特殊1 特殊2 ✔️ 成功
dismiss 特殊1 默认2 ✔️ 成功

奔溃1的问题是因为没有实现preferredInterfaceOrientationForPresentation,而默认结果是当前的statusBar的样式,从默认过去,那就是竖直方向,而这个界面supportedInterfaceOrientations的样式又是横屏,所以优先的方向(preferredxxx)不包含在支持的方向(supportedxxx)里就奔溃了。按照之前的约定,supportedInterfaceOrientations是必须实现的,实现了就成功了。

所以解决方案通过测试。

最后,present和push的切换方式有个不同:如果A--->B使用present方式,A不可旋转,但同时支持横竖屏,B可旋转,支持横竖屏,那么 A竖屏 ---> B竖屏 ---> 旋转到横屏 ---> dismiss 这个流程后,A会变成横屏且不可旋转。

也就是dismiss时,返回的界面不看你能不能旋转,如果你支持当前的方向,就会直接变成当前方向的样式。而supportedInterfaceOrientations默认是3个方向的,所以不实现这个方法而使用默认的,在dismiss的时候会有坑。