iOS屏幕旋转解决方案

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容