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 的时候会有坑.
来源: https://juejin.im/post/5bd14215e51d457a7515574d