如何用代码控制以不同屏幕方向打开新页面【iOS】

2015-8-1   /   字数:5411   /   阅读数:40749   /   分类: iOS & Swift     

代码示例:https://github.com/johnlui/Swift-On-iOS/tree/master/ControlOrientation/ControlOrientation

推荐简化方案:https://github.com/zyg-github/ControlOrientation

环境要求:Xcode 7 / Swift2.0

前两天遇到了一个 “使用指定的不同屏幕方向打开新页面” 的需求,需求很简单:APP 一直保持竖屏,要求新打开的页面能够指定为横屏或竖屏,并且不允许自动切换,新页面退出后要恢复竖屏。

准备工作

新建一个单页面项目,命名为 ControlOrientation。接下来取消 Landscape Right 的勾选,我们的 APP 将只支持 竖屏(Portrait)和 Landscape Left:

Image

以横屏打开新页面

给 Main.storyboard 拖入一个 View Controller,新建一个继承自 UIViewController 的类,名为 SecondViewController,然后将两者绑定。之后给新建的这个 View Controller 赋予 StoryBoard ID 值 “secondVC”:

简单 Google 便可以得到代码方式指定横屏打来新页面的方式:

在新页面的 viewDidLoad 中执行:

UIDevice.currentDevice().setValue(UIInterfaceOrientation.LandscapeLeft.rawValue, forKey: "orientation")

在启动页中间添加一个居中的按钮,拖动绑定点击事件,加入载入新页面的代码:

Image


@IBAction func openNewVC(sender: AnyObject) {
    if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("secondVC") as? SecondViewController {
        self.presentViewController(vc, animated: true, completion: nil)
    }
}

运行!查看效果:

Image

代码控制横屏成功!

还有一个收尾工作:给 SecondViewController 增加一个退出按钮,这一步就不再描述啦。然后再次运行项目,查看效果:

Image

出来之后也变横屏了哎,这不是我们想要的。

屏幕方向锁定

重读文章开头的需求,我们就会发现需求要求我们在任何一个界面都不能因为手机物理方向的变化而自动改变屏幕方向,稍微 Google 一下,我们就会得到锁死屏幕方向的代码:

给 ViewController 和 SecondViewController 都增加一个函数:

override func shouldAutorotate() -> Bool {
    return false
}

为了直观的给大家展示,我们将通过手动改变模拟器的方向来模拟真机屏幕方向的变化(改变模拟器屏幕方向的快捷键是 Command 加左或者右),检验效果:

Image

简直完美呀!是不是觉得有点太简单了?这么容易就实现了?当然不是,这么容易我还写个毛的文章呀。

神级 BUG

既然新打开横屏已经完美解决了,那么新打开竖屏怎么样呢?让我们把那一行切换屏幕方向的代码注释掉,运行查看结果:

Image

BUG 了!! 

解决 BUG

这个 bug 我搞了两天,虽然不是一直在搞它,但是总时长也至少有八个小时。其实严格意义上来讲,这并不是 bug,这只是我们不知道怎么才能搞定而已。最后我终于搞明白了这个 BUG 背后的运行原理,先说解决方案:

竖屏解决方案

在第一个页面 ViewController 中增加以下两个函数:

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.Portrait
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
    return UIInterfaceOrientation.Portrait
}

在第二个页面 SecondViewController 中增加两个函数:

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.Portrait
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
    return UIInterfaceOrientation.Portrait
}

查看效果:

Image

竖屏搞定!

横屏解决方案

将 SecondViewController 中的函数改为:

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.All
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
    return UIInterfaceOrientation.LandscapeLeft
}

别忘了打开 viewDidLoad 函数中的横屏控制代码哦。查看效果:

Image

横屏搞定!

全搞定了吗?并没有

我们还是 Too Young Too Simple,在我们简陋的例子中,似乎确实已经全搞定了,但是实际工程中大多数项目都不是普通 View Controller 作为根控制器哦~

我们测试一下 QQ、微信、微博 等 APP 采用的通用方案:根控制器为 TabBarController,之后嵌套 NavigationController,然后放入 ViewController 页面进行展示,然后 present 出 SecondViewController。

VIE TNV(TabbarController->NavigationController->ViewController) 架构

我们可不是跟风在国外上市的中国互联网公司拆 VIE 架构哦~

  1. 选中 ViewController,点击菜单中的 Editor -> Embed In -> Navigation Controller,第一层嵌套完成。
  2. 选中 NavigationController,点击菜单中的 Editor -> Embed In -> Tab Bar Controller,第二层嵌套完成。

如图:

Image

查看效果:

Image

这他妈什么玩意儿 o(╯□╰)o

终极解决方案

我埋坑的过程就不细说了,下面直接给出特性分析及解决方案:

特性分析

跟 shouldAutorotate() 不同,判断是否应该改变 APP 屏幕方向并不会检测当前显示的 View Controller 的属性,而是去检测根 View Controller 的属性,所以我们要从 TabBarController 一路获取到当前 View Controller。

解决方案

新建一个继承于 UITabBarController 的类,名为 TabBarController,将其和 StoryBoard 中的 Tab Bar Controller 页面绑定。该类完整代码如下:

import UIKit

class TabBarController: UITabBarController, UITabBarControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    override func shouldAutorotate() -> Bool {
        return false
    }
    func tabBarControllerSupportedInterfaceOrientations(tabBarController: UITabBarController) -> UIInterfaceOrientationMask {
        return self.selectedViewController!.supportedInterfaceOrientations()
    }
    func tabBarControllerPreferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
        return self.selectedViewController!.preferredInterfaceOrientationForPresentation()
    }
}

同样,新建一个继承于 UINavigationController 的类,名为 NavigationController,将其和 StoryBoard 中的 Navigation Controller 页面绑定。向该类添加以下代码:

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return self.visibleViewController!.supportedInterfaceOrientations()
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
    return self.visibleViewController!.preferredInterfaceOrientationForPresentation()
}

检查结果

Image

写在最后

通过本文我们可以总结出以下两点:

  1. 是否自动转换屏幕方向由当前显示的 View Controller 决定。
  2. 是否支持横屏和是否优先选择横屏由 rootViewController 决定,若有多层结构嵌套,则需要层层专递,将控制权交给当前显示的页面。类似于 代理传值 和 延迟静态绑定。

另外,我利用 Swift 的 enum 给 SecondViewController 类加上了一个优雅的 横屏/竖屏 控制开关,具体代码可以在 Github 上查看。


简化方案

感谢 zyg 提供了一个 TNV 架构下的简易解决方案,使用一个函数就能把 TabBarController 的所有子页面直接锁死:

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.Portrait
}

之后再需要 present 出的页面里面根据需要进行简单设置就可以达到目的了:

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return self.orientation == ScreenOrientation.LandscapeLeft ? UIInterfaceOrientationMask.LandscapeLeft : UIInterfaceOrientationMask.Portrait
}


推荐大家使用这个方案,我之前的方案走了弯路,其实大量代码是浪费的。

WRITTEN BY

avatar

评论:

manajay
2016-11-15 21:24
func tabBarControllerPreferredInterfaceOrientationForPresentation(_ tabBarController: UITabBarController) -> UIInterfaceOrientation {
        return self.selectedViewController!.preferredInterfaceOrientationForPresentation
    }
    
发现 刚进入 app 的时候 就解包失败了,崩溃


        // 解决 横屏的 selectedViewController 解包失败的问题
        self.selectedIndex = 0
Jack
2016-08-18 15:35
博主咨询下,我页面横竖屏完全是两套UI,要怎么实现切换好?
iqluoji
2016-06-23 11:05
博主我屏幕不会旋转 但是导航栏却要旋转 navC tabC viewC这3级都用了这两个方法 还是不行....
JohnLui
2016-06-23 11:21
@iqluoji:我感觉自定义一个侧边导航栏对你来说更适合。。。
葱泥
2016-06-15 15:36
我的iPhone  可以强制横屏了
但是在ipad上就没有强制横屏,咋回事
JohnLui
2016-06-15 15:38
@葱泥:iPad 跟 iPhone 好像还有些差别,我没搞过。。。
葱泥
2016-06-15 15:40
@JohnLui:呃,我都不知道该咋办了
葱泥
2016-06-16 08:50
@JohnLui:呃,我都不知道该咋办了
有没有什么好办法?
黑木宁
2016-04-22 15:46
必须是present才可以吗?  
为什么push就不行了嫩
lll
2016-03-09 11:38
zyg 的方式其实是没有用到UINavigationController的  只能present viewcontroller , 不支持用push方式
其实就是你  "全搞定了吗?并没有" 部分之前的解决方式
zyg
2015-09-16 22:24
有点笨 不太看懂最后的总结~  
按照你教程做没啥问题  不过 还是不能随心所欲的控制方向

有几个问题 没能搞懂:
1、一个界面 打开时的方向由什么决定的  (应该不是 UIDevice.currentDevice().setValue(UIInterfaceOrientation.LandscapeLeft.rawValue, forKey: "orientation")   这种强制的吧)
2、当一个界面消失的时候 下层的界面的方向 由谁决定的?
3、supportedInterfaceOrientations  、 preferredInterfaceOrientationForPresentation  、 tabBarControllerSupportedInterfaceOrientations 和 tabBarControllerPreferredInterfaceOrientationForPresentation  这个几个方法 影响着什么?
JohnLui
2015-09-16 23:52
@zyg:1. 本质上由 supportedInterfaceOrientations  、 preferredInterfaceOrientationForPresentation 决定,UIDevice.currentDevice().setValue 只有在竖屏,上述两个方法出现多项选择时辅助旋转一下屏幕,横屏下没作用。
2. 下层的方向由他自己决定,这个工程中我们把下层锁定了竖屏。
3. 前两个是主要功能型函数,后两个只是为了传递权限申请。
zyg
2015-09-17 21:50
@JohnLui:hello 经过试验 要实现你的需求 只需要 复写 supportedInterfaceOrientations
    override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
         //按需求在不同控制器 修改方向
        return UIInterfaceOrientationMask.Portrait
    }
其他 什么都不需要

如果是多级嵌套  回到rootViewController 复写  supportedInterfaceOrientations
JohnLui
2015-09-17 21:57
@zyg:这么屌?把代码传 Github,我试试
zyg
2015-09-19 09:16
@JohnLui:发给你了 看了吗?  符合你需求吗?
JohnLui
2015-09-19 12:51
@zyg:没看到呀,发哪里了?
zyg
2015-09-19 19:05
@JohnLui:github地址:  https://github.com/zyg-github/ControlOrientation
你那个评论还真奇怪  一定要输入中文    结果以前提交不成功 (提交不成功 跟 提交成功 提示还真像 而且字有点小 呼呼)
JohnLui
2015-09-19 20:59
@zyg:牛逼

在 TNV 架构下,你用 TabBarController 里的 supportedInterfaceOrientations 函数直接把所有页面直接锁死,而 present 出的 ViewController 不属于 TabBarController,厉害

你这确实是一个不错的简化方案,我把你这个解决方案加到文章中去。
zyg
2015-09-19 21:46
@JohnLui:其实比较简单粗暴  哈哈~    
Lio
2017-11-09 13:41
@JohnLui:zyg 这个方案测试了下并不靠谱啊
ChiHo
2015-08-11 20:37
xc7b5还在下,还没有跑你的demo。但是我觉得这个代码应该有一个坑爹的地方是,如果是universal的ipad横屏下打开应用是没法强制竖屏的。我找了很久还是没找到方法。
ChiHo
2015-08-12 14:32
@ChiHo:刚刚试了一下,果然是不行的。T^T
dee
2015-08-04 09:15
好文章, 总结了我一直搞不清的问题
songxing10000
2015-08-01 21:40
前排沙发,博主终于又出新帖了
zyg
2015-08-01 21:17
运行代码报错 xc6.4
JohnLui
2015-08-01 23:35
@zyg: 忘了写系统版本要求了,要求 Xcode 7 / Swift 2.0。
zyg
2015-09-16 21:26
@JohnLui:hello  最终还是不了解 结论?

发表评论:

© 2011-2024 岁寒  |  Powered by Emlog