一、前言
Swift版本 4.0
Xcode版本 9.2
以前接觸到的專案需求中,幾乎都是全豎屏展現介面,所以我也來得省事,直接在TARGETS
中的介面方向選項中只勾選豎屏,這樣就滿足了需求。
但最近的專案中,產品突然增加了一個需求,需要部分介面支援旋轉,這才來研究了一下螢幕旋轉的問題!
需要緊急解決問題的道友直接看3.3
二、螢幕旋轉相關知識
2.1 三個方向的理解和聯絡
- UIDeviceOrientation: 裝置方向
public enum UIDeviceOrientation : Int {
case unknown
case portrait // 裝置vertically方向, home鍵在下方
case portraitUpsideDown // 裝置vertically方向, home鍵在上方
case landscapeLeft // 裝置horizontally方向, home鍵在右方
case landscapeRight // 裝置horizontally方向, home鍵在左方
case faceUp // 裝置flat方向, 螢幕朝上
case faceDown // 裝置flat方向, 螢幕朝下
}
複製程式碼
從裝置方向的命名就能看出來這個列舉的含義,這裡指的是物理裝置(即iPhone
)的方向。
- UIInterfaceOrientation: 介面方向
public enum UIInterfaceOrientation : Int {
case unknown
case portrait
case portraitUpsideDown
case landscapeLeft
case landscapeRight
}
複製程式碼
而介面方向指螢幕中顯示內容的方向,它的方向和Home鍵的方向是一致的。仔細觀察一下螢幕旋轉就能理解UIDeviceOrientation
和UIInterfaceOrientation
了,我們把手機轉向左邊,可以看到介面隨之才轉向右邊。
- UIInterfaceOrientationMask: 是用來控制允許轉向的方向,對應
UIInterfaceOrientation
public struct UIInterfaceOrientationMask : OptionSet {
public init(rawValue: UInt)
public static var portrait: UIInterfaceOrientationMask { get }
public static var landscapeLeft: UIInterfaceOrientationMask { get }
public static var landscapeRight: UIInterfaceOrientationMask { get }
public static var portraitUpsideDown: UIInterfaceOrientationMask { get }
public static var landscape: UIInterfaceOrientationMask { get }
public static var all: UIInterfaceOrientationMask { get }
public static var allButUpsideDown: UIInterfaceOrientationMask { get }
}
複製程式碼
2.2 觀察螢幕旋轉並作出響應
2.2.1 觀察裝置方向並響應
// 沒有生成通知
if !UIDevice.current.isGeneratingDeviceOrientationNotifications {
// 生成通知
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
}
// 鎖定豎屏,依然有效,例如faceUp.
NotificationCenter.default.addObserver(self,
selector: #selector(handleDeviceOrientationChange(notification:)),
name:NSNotification.Name.UIDeviceOrientationDidChange,
object: nil)
複製程式碼
@objc private func handleDeviceOrientationChange(notification: Notification) {
// 獲取裝置方向
let orientation = UIDevice.current.orientation
switch orientation {
case .landscapeRight:
// iOS8之後,橫屏UIScreen.main.bounds.width等於豎屏時的UIScreen.main.bounds.height
print(UIScreen.main.bounds.width)
print("landscapeRight")
default: break
}
}
複製程式碼
登出
deinit {
NotificationCenter.default.removeObserver(self)
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
複製程式碼
2.2.2 觀察介面方向並響應
和上面類似不過觀察的name
為
// 鎖定豎屏,無效,通知方法不會觸發
NSNotification.Name.UIApplicationWillChangeStatusBarOrientation
NSNotification.Name.UIApplicationDidChangeStatusBarOrientation
複製程式碼
獲取介面方向
let statusBarOrientation = UIApplication.shared.statusBarOrientation
複製程式碼
2.2.3 建議
這裡建議監聽介面方向,原因有二:
- 監聽裝置方向,會返回多個方向,例如
portrait
和faceUp
不衝突。 - 監聽裝置方向,上面提到,先是裝置旋轉,隨之介面旋轉,這裡就有一個問題,我們操作介面時,可能介面還沒有旋轉。
三、問題解決實戰
需要實現部分介面可旋轉,部分介面鎖定豎屏,首先我們需要配置TARGETS
中的Device Orientation
,這裡是總開關,預設勾選瞭如圖方向:
Protrait
完事,不過像我現在這樣,可能突然一個需求改變就不得不繼續適配,哈哈。
這裡的配置不要和程式碼控制的方向相沖突,不然會引發奔潰。
3.1 控制螢幕旋轉的函式
// 預設為true
override var shouldAutorotate: Bool {
return true
}
// 支援的旋轉方向
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscapeLeft
}
// 模態切換的預設方向
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .landscapeRight
}
複製程式碼
這三個屬性都重寫的UIViewController
的屬性。哎,看到模態切換,這裡再給自己挖坑一個,以前研究了一會模態切換,只不過沒寫成總結,後面會寫出來(:。
並且這三個方法會受到控制器層級的影響,也就是如果當前控制器配置支援旋轉,如果他的導航控制器,乃至Tabbar
控制器不支援旋轉,當前控制器的配置也不會生效。
3.2 不同根控制器情況下的解決
核心問題: 需要旋轉的介面是少數,大多介面需要鎖定豎屏。
3.2.1 根控制器為UIViewController
對應Demo
配置:
BaseVC
,在其中的配置鎖定豎屏:
class BaseVC: UIViewController {
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
複製程式碼
然後其餘控制器繼承BaseVC
,需要旋轉的控制器單獨再次重寫方法。
3.2.2 根控制器為UINavigationController
對應Demo
配置:
UINavigationController
class BaseNavC: UINavigationController {
override var shouldAutorotate: Bool {
return self.viewControllers.last?.shouldAutorotate ?? false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return self.viewControllers.last?.supportedInterfaceOrientations ?? .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return self.viewControllers.last?.preferredInterfaceOrientationForPresentation ?? .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
複製程式碼
3.2.3 根控制器為UITabBarController
對應Demo
配置:
class BaseTabBarC: UITabBarController {
override var shouldAutorotate: Bool {
return self.selectedViewController?.shouldAutorotate ?? false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return self.selectedViewController?.supportedInterfaceOrientations ?? .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return self.selectedViewController?.preferredInterfaceOrientationForPresentation ?? .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
複製程式碼
同理,我們只需要獲取當前選中的控制器的配置賦給UITabBarController
,這樣一層一層就配置好了!
3.3 最簡單的實現方式
對應Demo
配置:
Appdelegate
中的:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?)
-> UIInterfaceOrientationMask {
}
複製程式碼
然後我立馬想到一個超級簡單的方法,那就是定義一個全域性變數或者快取一個bool值來進行判斷,如下:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?)
-> UIInterfaceOrientationMask {
if isAllowAutorotate {
return [.portrait, .landscapeLeft, .landscapeRight]
}
else {
return .portrait
}
}
複製程式碼
然後預設isAllowAutorotate
這個全域性變數為false
,在需要旋轉的控制器中:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
isAllowAutorotate = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
isAllowAutorotate = false
}
}
複製程式碼
這樣就不用麻煩的去搞那些繼承什麼的了!
3.4 補充(回答評論問題)
首先多謝評論中的 @SuperDanny
指出這個問題
這裡我確實欠考慮。順便討論一下我開始想寫但沒寫問題:
a. 橫屏跳豎屏
b. 豎屏跳橫屏
複製程式碼
下面分情況來討論:
- 1.如果是
Present
模態切換
對於 a
和 b
兩種情況,我們都可以直接重寫進入的控制器 preferredInterfaceOrientationForPresentation
屬性,雖然視覺效果看上去不那麼好。a
情況很少,b
的情況還是很常見的,例如跳入一個 全屏播放的視訊播放器的控制器,這種情況可以自定義模態切換的動畫或者直接進行旋轉動畫。
override var preferredInterfaceOrientationForPresentation:
UIInterfaceOrientation {
return .portrait
}
複製程式碼
- 2.如果是
Push
切換
在進入的控制器中:
override var shouldAutorotate: Bool {
return true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 切換到你想要的方向
driveScreen(to: .portrait)
}
func driveScreen(to direction: UIInterfaceOrientation) {
UIDevice.current.setValue(direction.rawValue, forKey: "orientation")
}
複製程式碼
值得注意的是,記得重寫 supportedInterfaceOrientations
設定允許跳轉的方向, Demo
已經更新。
總之靈活使用上面的這些方法,達到產品要求的目的,還有就是不要忘記自定義模態切換動畫或者直接進行旋轉動畫也能達到介面旋轉的視覺效果,儘管此時的方向依舊是 portrait
!
四、後記和Demo
參考:
How to force view controller orientation in iOS 8?