iOS 开发技巧 - 手动控制屏幕UI方向

iOS 开发过程中,特别是视频类app,经常有手动控制方向的需求。然而官方并没有提供很方便快捷的接口。之前看了一篇博客 如何用代码控制以不同屏幕方向打开新页面【iOS】 。其中的概念讲了很多关于方向的点,但是没有讲到手动控制屏幕方向。

一般实现

最基本的屏幕旋转其实就需要设置俩地方

1
2
3
4
5
6
7
override var shouldAutorotate: Bool {
return true
}

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return [.all]
}

然后调用

1
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")

现实世界的实现

然而产品怎么会这么简单就放过你,实际项目永远比这个复杂几百倍。首先实际项目基本都是 Nav -> Tabbar -> Nav -> VC 这样结构。这种结构怎么设置请参考 如何用代码控制以不同屏幕方向打开新页面【iOS】 和我的 Demo ScreenOrientBugDemo

实际需求一般是这样

  1. 可以开启和关闭自动旋转开关
  2. 可以手动旋转至指定状态

其中需求 1 好实现,直接 shouldAutorotate return false 即可。但是这个导致另一个问题,没法手动旋转屏幕。

为了解决这个,有了以下方案。先上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class TargetViewController: UIViewController {
var allowRotate = false

// 每次调用 shouldAutorotate 时返回 allowRotate 的值
override var shouldAutorotate: Bool {
get {
return allowRotate
}
}

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return [.all]
}

// 当调用 rotate(to: UIInterfaceOrientation.landscapeLeft) 时,先允许自动旋转,然后进行屏幕旋转。再设置为不允许自动旋转。
func rotate(to orientation: UIInterfaceOrientation) {
allowRotate = true
UIDevice.current.setValue(orientation.rawValue, forKey: "orientation")
allowRotate = false
}

神 Bug

每当认为自己代码务必牛逼毫无问题,就会出现各种神 bug 来打脸。这次也不可避免的遇到了一个神 Bug。具体表现

  • 一般情况屏幕旋转正常
  • 先点击横屏,使手机屏幕显示为横屏。然后拿着手机转一圈,使手机设备处于竖屏方向,然后点击竖屏就会一直处于下图状态。无法旋转为竖屏 UI。


其实为了解决这个,我们先理一下我们上述代码和系统屏幕旋转的处理逻辑就能找到问题。

屏幕旋转的整个流程是如下

  1. 当当前设备方向属性 orientation 发生变化时候,会调用 Controller 的 shouldAutorotate 属性。
  2. 如果 shouldAutorotate 为 true 则会进一步调用 supportedInterfaceOrientations 来查询支持的屏幕方向。
  3. 当 Controller 支持的方向和设备方向一致时候就进行旋转操作。

我们这个神 Bug 就出在第一步。UIDevice 的 orientation 属性并不是指的屏幕的方向,而是设备的方向。我们屏幕旋转的实现就是通过手动修改 orientation 属性来欺骗设备告诉他设备方向发生了变化,从而开始屏幕旋转流程。

如果当屏幕 UI 处于横屏且 shouldAutorotate = false 时候,我们旋转手机设备 orientation 属性会持续变化并开始屏幕旋转流程调用 shouldAutorotate。但是因为 shouldAutorotatefalse 所以不会有任何反应。

当屏幕 UI 处于横屏且我们旋转手机设备至竖屏状态时, orientation 属性已经为 UIInterfaceOrientation.portrait.rawValue 了,所以此时再次设置为 UIInterfaceOrientation.portrait.rawValue 并不会调用被系统认为屏幕方向发生了变化。所以就不会有任何变化。

既然知道了原因,解决办法很简单。只需要每次改造一下函数

1
2
3
4
5
6
7
func rotate(to orientation: UIInterfaceOrientation) {
allowRotate = true
// 先自己给一个值0,然后再修改为其他屏幕方向就能顺利调起 KVO 来进入屏幕旋转流程。
UIDevice.current.setValue(UIInterfaceOrientation.unknown.rawValue, forKey: "orientation")
UIDevice.current.setValue(orientation.rawValue, forKey: "orientation")
allowRotate = false
}