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的时候会有坑。