iOS开发中的横屏问题

作者:代培
地址:http://blog.csdn.net/dp948080952/article/details/53701726
转载请注明出处

前言

最近项目中遇到一个问题,需要使某些界面强制横屏,某些界面只能竖屏,比较难的地方是在于需要横屏的界面的VC是拿不到的,只能通过一些非正常的手段来做到,虽然能够解决,不过却不是非常优雅的方法,但是在探索的过程中却对iOS中的屏幕方向问题有所了解,在这里写篇博客记录下来。

正文

横屏概况

首先说说怎么让界面横屏显示,我觉得可以分为两个方面,一个是主动,一个是被动,主动就是你没有改变手机的方向通过代码改变屏幕的显示方向,被动就是当你弹出或是启动应用时屏幕的方向。

  • 主动
[[UIDevice currentDevice] setOrientation: UIInterfaceOrientationLandscapeRight]

这是方法以前是公开的,现在已经是私有API了,不过我们仍然可以去使用这个方法。

if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
        [[UIDevice currentDevice] performSelector:@selector(setOrientation:)
                                       withObject:(id)UIInterfaceOrientationLandscapeLeft];
}

还有一个方法有些类似,是改变状态栏的方向,这个方法已经不推荐使用了,不过暂时还可以用。

[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft animated:YES];
  • 被动

上面的主动的方式如果要能实现必须要有被动的支持,如果没有被动的支持,那些方法调用以后一点效果都没有。

被动的有分为两个层级,一个是应用级,一个是VC级,我们看一下具体的代码:

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    return UIInterfaceOrientationMaskAll;
}

这是应用级,如果你这个地方返回的是UIInterfaceOrientationMaskLandscape那么整个应用都只支持横屏,无法竖屏(当然也有例外,这个后面再说),注意这个是UIApplicationDelegate协议中定义的方法,所以这些代码应该写在AppDelegate中。

当然除了代码我们也可以在工程里进行设置,如下图所示,勾选了全部相当于前面代码中返回UIInterfaceOrientationMaskAll,当然如果一个也不勾那么效果和现在也是相同的,需要注意的是这里的勾选会被上面的代码覆盖,如果两个同时都有那么以代码中的返回值为准。

对于VC级的代码如下:

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscape;
}

这是UIViewControllerUIViewControllerRotation分类中定义的方法,我们需要在VC中重写该方法,返回你需要的方向,上面的代码表明该VC只会横屏显示。

全局屏幕方向控制

说完关于屏幕方向的基础知识来说一下工程中如何优雅的控制屏幕的方向,首先说一下全局的控制方案,就是在AppDelegate中进行控制,通过控制- application: supportedInterfaceOrientationsForWindow:的返回值来控制屏幕的方向。

如果应用需要在某几个页面强制横屏,在某些页面只要竖屏,那可以使用类似下面的代码:

//UIViewController+TopVC.m

@implementation UIViewController (TopVC)

+ (UIViewController *)topMostViewController {
    UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;

    return [self topViewControllerWithRootViewController:rootViewController];
}

+ (UIViewController *)topViewControllerWithRootViewController:(UIViewController *)rootViewController {

    if ([rootViewController isKindOfClass:[UITabBarController class]]) {
        UITabBarController* tabBarController = (UITabBarController*)rootViewController;
        return [self topViewControllerWithRootViewController:tabBarController.selectedViewController];
    } else if ([rootViewController isKindOfClass:[UINavigationController class]]) {
        UINavigationController* nav = (UINavigationController*)rootViewController;
        return [self topViewControllerWithRootViewController:nav.visibleViewController];
    } else if (rootViewController.presentedViewController) {
        UIViewController* presentedViewController = rootViewController.presentedViewController;
        return [self topViewControllerWithRootViewController:presentedViewController];
    } else {
        return rootViewController;
    }
}

@end


//AppDelegate.m

@interface AppDelegate ()

@end

@implementation AppDelegate

...

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    if ([[UIViewController topMostViewController] isKindOfClass:[DPViewController class]]) {
        return UIInterfaceOrientationMaskLandscape;
    }
    return UIInterfaceOrientationMaskPortrait;
}
@end

就是获取最上层的VC,然后根据VC的类名来控制屏幕的方向。这个方法适合应用中有极少数界面的方向不同于其他界面。

局部屏幕方向控制

  • 继承

这里局部的意思是每个VC控制自己的方向,这其实很好理解,如果一个VC只能竖屏,那么-supportedInterfaceOrientations方法就返回竖屏,只能横屏那么就返回横屏,有人觉得写太累了,每个VC都要重写这个方法,不过这样有一个好处,那就是能够应付应用界面方向很多种的情况,可以有一个简化的方式那就是建几个基础的基类:

// DPLandscapeViewController.m

@interface DPLandscapeViewController ()

@end

@implementation DPLandscapeViewController

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscape;
}

@end


// DPPortraitViewController.m

@interface DPPortraitViewController ()

@end

@implementation DPPortraitViewController

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

@end

例如建一个竖屏和一个横屏的基类,后面需要竖屏显示的VC都继承第一个类,需要横屏显示的都继承第二个类,这样就不用在每个VC中都重写这个方法了。

这个方法适用于屏幕方向有许多种的应用,不过这样的应用应该比较少吧:)

  • 分类

除了使用继承的方式,还可以使用分类的方法,这样感觉会更好些,不需要全部都继承一个类,也更加灵活,不需要在应用开发的初期就考虑好全部,在开发过程中也可以改变VC的屏幕方向,可以用类似下面的代码来实现:

@implementation UIViewController (Orientation)

-(NSUInteger) supportedInterfaceOrientations
{
    if ([[self class] isSubclassOfClass:[XXXViewController class]]) {
        return UIInterfaceOrientationMaskLandscape;
    }
    return UIInterfaceOrientationMaskPortrait;
}

在Category重写这个方法,根据不同的VC来设置不同的屏幕方向,这样很方便也很容易实现,但是如果这样写就不要在VC里再重写这个方法,否则这个方法就不会被调用。

这个方法适用于应用中少数界面的方向与其他界面不同,当然即使应用里界面方向各异用这个方法也不是不行,个人觉得这是最优雅的方案,不用继承,不用在每个VC中都重写一个方法,灵活自由,后期修改也很方便。

注意

在介绍完上面两种方法后,有一个需要注意的点,这里有一个坑,十分的坑,那就是这两个方法千万不要同时使用,什么意思呢,如果在VC中重写了-supportedInterfaceOrientations方法,那么在AppDelegate中千万不要重写关于方向支持的方法,也不要在工程设置里勾选任意一个方向,否则你可能会遇到下面这个错误:

*** Terminating app due to uncaught exception 'UIApplicationInvalidInterfaceOrientation', 
reason: 'Supported orientations has no common orientation with the application, 
and [DPViewController shouldAutorotate] is returning YES'

什么原因呢?意思大致就是你的应用不支持某个方向,但是你的VC非要在那个方向上显示,就产生了这个错误,比如应用只支持竖向,而你的VC只支持横向,那么这个时候就冲突了。

避免的方法很简单那就是不要同时重写两个方法,因为你很难去控制不冲突,至少说是会有这个隐患的,而我的建议是尽量不要使用全局的方法,因为如果别人添加一个VC的时候万一不知道而重写了-supportedInterfaceOrientations方法,那么就极其容易引起崩溃,所以这是个隐患,要尽量去避免。

这里为什么说极其容易引起崩溃呢?这里给大家举个例子,在这个例子中,看似不会崩溃的代码实际上却会崩溃,大家准备好张大嘴吧!

在AppDelegate中我们这样重写

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    if ([[UIViewController topMostViewController] isKindOfClass:[DPViewController class]]) {
        return UIInterfaceOrientationMaskLandscape;
    }
    return UIInterfaceOrientationMaskPortrait;
}

在DPViewController中我们这样重写

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscape;
}

在其他ViewController中我们都这样重写

(UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

从代码的逻辑来看这样是没有问题的,在DPViewController下都设为横屏,其他都设为竖屏,但实际上这样是会崩溃的,崩溃的原因就是上面的原因,是不是很惊讶,为啥呢?这就跟它底层的实现有关了。

-[ViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[ViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[ViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[DPViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[DPViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[ViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[DPViewController supportedInterfaceOrientations]

我们来看从ViewController present DPViewController时的打印情况,首先会调用ViewController的supportedInterfaceOrientations方法,然后是AppDelegate,然后反复进行了3次,3次可能是经历了一个确认阶段,这时DPViewController还没被present出来,然后就是调用DPViewController和AppDelegate的方法,这时候最上层的VC已经变为DPViewController,所以AppDelegate的返回值已经是横屏了,这时比较有趣的是系统用调用了一次ViewController的supportedInterfaceOrientations方法,就是这样引起了冲突,导致了崩溃,所以这个崩溃的原因是很奇怪的,是因为这些方法古怪的调用顺序造成的,但是这个既然是这样肯定有他的道理,存在即合理,何况是苹果的实现。

综上我想说,全局的方向控制还是少用,即使要用也要完全理解了,尽量减少应用崩溃的风险。

UINavigationController的屏幕方向

UINavigationController的情况是比较特殊的,对于UINavigationController来说,他的子VC的屏幕方向是跟随UINavigationController的,就是说即使你的子VC重写了supportedInterfaceOrientations方法,但当这个VC被push出来时,其方向也不会是你设定的方向。

这个情况该如何解决呢,如果你想让push出来的VC具有present出的VC同样的属性,即返回啥方向就按照啥方向来显示,那么你需要继承UINavigationController在写一个类重写其supportedInterfaceOrientations方法,可以写成类似下面的样子:

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

让UINavigationController的方向取决于最上层的子VC,不过这样还是有一个问题,假设开始是竖屏的,你push一个设置为横屏的VC,可是push出来后仍然是竖屏,这时你要把手机横过来,VC才能变为横屏,这就比较尴尬了,不符合我们之前的预期!不要急,这个问题肯定能够解决的。

还记得文章开头时所写的主动转屏的方法嘛,这里就能派上用场了,虽然是私有API,但是能解决问题,在push方法结束后让设备变为横屏,这样就没问题了。

其实这里调用这个方法就相当于你模拟了将手机横过来的操作,我猜测苹果在检测到手机角度发生变化时就是调用那个私有API,完成屏幕的跟随旋转,所以这也就能解释通为何这个方法会变成私有API,因为这个API对于开发者来说应该是没有应用场景的,因为你是没有权利去模拟一个旋转手机的操作,所以这个方法对于开发者不应该开放。

当然除了这个解决方案,stackoverflow上也给出了不同的解决方案:UINavigationController Force Rotate,大家可以参考一下,觉得哪个好就用哪个!

总结

以上是在解决问题的过程中学习到的和自己的总结思考,如果有错误希望大家能够指出。其实我真正要解决的问题并没有解决,前面还说到一个问题:当你把应用设为只能竖屏也不能保证应用里不出现横屏的界面。我就遇到过,当我在接入一家视频广告的时我发现在一个只许横屏的应用中,这个视频仍然能够横屏播放却不会出错,这让我很好奇他是怎么做到的,如果有人知道希望大家留言告诉我,不胜感激!不过我也会持续研究,如果有什么新发现,还会写一篇博客分享。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值