项目中单个页面横屏的需求
在许多项目中,大部分页面是竖屏(Portrait)个别页面横屏(Landscape)的需求经常出现。 我们的项目中有两种需求:
- 大部分页面是竖屏的,个别页面可以横竖旋转屏幕(例如:PDF文件查看)
- 大部分页面是竖屏的,个别页面只能横屏(例如:视频播放) 下面将总结下我对这两种需求的处理。
个别页面可以横竖屏旋转的需求
屏幕旋转的实现,总是让人感觉晕晕的,主要是没有很明确的文档介绍屏幕旋转的规则。 根据文档,最通用的全局设置屏幕方向的方式是用plist。 读取 Info.plist 里面的 UIInterfaceOrientation 的值作为全局的屏幕方向。 如果设置了 AppDelegate 里面这个方法
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window;
就会用这个方法返回的值来决定屏幕的方向,不再使用plist里面规定的屏幕方向。
但是,UIViewController 中也有与屏幕旋转相关的代码
//是否允许转屏
- (BOOL)shouldAutorotate
//支持的显示方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
//viewController初始显示的方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
这个就比较晕了,到底哪个方法最终决定ViewController出现的方向呢? 当我看
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window;
在这个方法时,说明文档里面有个Discussion
This method returns the total set of interface orientations supported by the app. When determining whether to rotate a particular view controller, the orientations returned by this method are intersected with the orientations supported by the root view controller or topmost presented view controller. The app and view controller must agree before the rotation is allowed.
从这段文字来看,当前设备的方向取决于 window 的 rootViewController 的方向。和 presented 的 topmost 的ViewController 的方向。 这里就有个问题了,presented 具体是怎么理解的? 一种意思是特指调用下面方法
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion NS_AVAILABLE_IOS(5_0);
present 出来的处于模态框状态的VC。
还有一种意思是泛指当前出现在屏幕上面的,最后出现(present)的VC。
经过测试,我感觉第一种理解是正确的。
那么,不是present出来的VC指定方向的时候,实现
- (UIInterfaceOrientationMask)supportedInterfaceOrientations;
并没有什么用,因为文档中已经说了,系统只参考rootViewController返回的方向。
这也就是某些文章中说,个别需要横屏的VC,添加到 UITabbarViewController 或者 UINavigationViewController 以后,再实现这个方法无效的问题。
并不是 UITabbarViewController 和 UINavigationViewController 有什么特殊,而是因为他们才是当前的rootViewController。
在我们App中,rootViewController是一个UITabBarViewController,然而在TabBar中重写了这个方法
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAll;
}
而且在 AppDelegate 中实现这个方法
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{
return UIInterfaceOrientationMaskPortrait;
}
这样之后,依然不能旋转。
看起来,系统决定当前屏幕方向的规则是
- keyWindow 必须支持该方向
- rootViewController 必须支持该方向
- present 的ViewController 必须支持该方向 只有这三个条件同时满足时,某个方向才是一个可用的方向。(当然,如果没有 present 出来的 VC 只要满足前两个条件就可以了)
present 模态窗口时需要注意的点 这里需要注意的是,如果 present 出来的 VC 实现了
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
这个方法,但是屏幕方向不支持这个方法中返回的orientation,会报错:
Trapped uncaught exception 'UIApplicationInvalidInterfaceOrientation', reason: 'Supported orientations has no common orientation with the application, and [VideoPlayerRotationHelpViewController shouldAutorotate] is returning YES'
弹出模态框报错了
把话题拉回来,上文说道,决定一个VC能支持的方向的规则是keyWindow支持该方向+rootViewController支持该方向
然而,在现实开发中,我们希望屏幕的方向由 topmost 的 ViewController ,看起来这个需求没法直接通过系统的API来实现了。 最终,我们实现的方法是
- 在 AppDelegate 里面
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{ if ([window isKeyWindow] == YES && window.rootViewController != nil) { return [window.rootViewController supportedInterfaceOrientations]; } return UIInterfaceOrientationMaskAll; }
这里的 rootViewController 是自定义的UITableBarViewController
- 在 UITabBarViewController 里面找到 topmost 的ViewController 就可以让 topmost 的ViewController 决定屏幕方向了
```
- (BOOL)shouldAutorotate{ if ([self.selectedViewController isKindOfClass:[RiceNavigationController class]]) { RiceNavigationController* tempController = (RiceNavigationController*)self.selectedViewController; if ([tempController.topViewController respondsToSelector:@selector(shouldAutorotate)]) { return [tempController.topViewController shouldAutorotate]; } }else if ([self.selectedViewController respondsToSelector:@selector(shouldAutorotate)]){ return [self.selectedViewController shouldAutorotate]; } return YES; }
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{ if ([self.selectedViewController isKindOfClass:[RiceNavigationController class]]) { RiceNavigationController* tempController = (RiceNavigationController*)self.selectedViewController; if ([tempController.topViewController respondsToSelector:@selector(supportedInterfaceOrientations)]) { return [tempController.topViewController supportedInterfaceOrientations]; } }else if ([self.selectedViewController respondsToSelector:@selector(supportedInterfaceOrientations)]){ return [self.selectedViewController supportedInterfaceOrientations]; } return UIInterfaceOrientationMaskAll; } ``` 这样一来,只要 topmost 的 ViewController 实现了 supportedInterfaceOrientations 这个方法,无论是AppDelegate 中的屏幕旋转还是rootViewController中屏幕旋转相关的方法,返回的屏幕方向都是 topmost 的ViewController 决定的。 这样就能很方便的决定某个 ViewController 的方向了。
将个别页面强行设置成横屏的需求
将单个页面设置成横屏一般有两种思路。一种是修改页面的transform,另一种是修改设备的方向。 在- (void)viewDidAppear 中调用如下代码:
UIDeviceOrientation currentOrientation = [UIDevice currentDevice].orientation;
NSNumber *orientationTarget = [NSNumber numberWithInt:UIDeviceOrientationLandscapeLeft];
[[UIDevice currentDevice] setValue:orientationTarget forKey:@"orientation"];
这种使用KVC的方式修改私有变量,会触发系统对 ViewController 的方向刷新。 但是上述代码有两个问题:
- 如果当前设备的真实物理方向是 LandscapeLeft ,那么用这种方式并不会触发系统对 VC 方向刷新,屏幕方向转不过来。
- 因为系统中记录的设备方向与设备真实的物理方向不一样,会在之后的操作中埋下坑。有可能在当前 VC 消失(popup)时,前一个不允许横屏的 VC 变成横屏。 为了改变这种管杀不管埋的坑方式,把屏幕旋转的相关代码控制在当前的 VC 里面,可以把上述代码稍微修改一下:
UIDeviceOrientation currentOrientation = [UIDevice currentDevice].orientation;
NSNumber *orientationTarget = [NSNumber numberWithInt:UIDeviceOrientationLandscapeLeft];
if (currentOrientation == UIDeviceOrientationLandscapeLeft) {
orientationTarget = [NSNumber numberWithInt:UIDeviceOrientationLandscapeRight];
}
[[UIDevice currentDevice] setValue:orientationTarget forKey:@"orientation"];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSNumber* orientationTarget = [NSNumber numberWithInt:currentOrientation];
[[UIDevice currentDevice] setValue:orientationTarget forKey:@"orientation"];
}
});
让系统调用 VC 的屏幕方向相关代码刷新屏幕方向之后,设备的物理方向与系统记录的方向相同,不会影响到之后的操作。
还有一种方法,present一个 UIViewController。系统在模态窗口出现与消失时,会刷新当前 VC 的方向。 在- (void)viewDidAppear 这个方法中写如下代码:
if (self.orientationChanged == NO) {//因为模态窗口消失的时候,会再次调用当前 VC 的 viewDidAppear
self.orientationChanged = YES;
VideoPlayerRotationHelpViewController* vc = [[VideoPlayerRotationHelpViewController alloc]init];
[self presentViewController:vc animated:NO completion:nil];
[vc dismissViewControllerAnimated:NO completion:nil];
}else{
//做正常的事情
}
这种方式没有用到什么黑科技,但是在 present 的 VC 上再次 present VC 会报错。
还有一种方式,就是把需要横屏的 VC 用 present 的方式显示出来。
https://stackoverflow.com/questions/15519486/presenting-navigation-controller-in-landscape-mode-is-not-working-ios-6-0
https://www.v2ex.com/t/97577