iOS 橫豎屏旋轉總結

swordjoy發表於2018-01-18

一、前言

Swift版本 4.0

Xcode版本 9.2

以前接觸到的專案需求中,幾乎都是全豎屏展現介面,所以我也來得省事,直接在TARGETS中的介面方向選項中只勾選豎屏,這樣就滿足了需求。

APP鎖定豎屏

但最近的專案中,產品突然增加了一個需求,需要部分介面支援旋轉,這才來研究了一下螢幕旋轉的問題!

需要緊急解決問題的道友直接看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鍵的方向是一致的。仔細觀察一下螢幕旋轉就能理解UIDeviceOrientationUIInterfaceOrientation了,我們把手機轉向左邊,可以看到介面隨之才轉向右邊。

  • 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 建議

這裡建議監聽介面方向,原因有二:

  • 監聽裝置方向,會返回多個方向,例如portraitfaceUp不衝突。
  • 監聽裝置方向,上面提到,先是裝置旋轉,隨之介面旋轉,這裡就有一個問題,我們操作介面時,可能介面還沒有旋轉。

三、問題解決實戰

需要實現部分介面可旋轉,部分介面鎖定豎屏,首先我們需要配置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配置:

iOS 橫豎屏旋轉總結
這種情況的APP可以說是非常少了,不過還是對後面的情況有所幫助。 設定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配置:

iOS 橫豎屏旋轉總結
我們可以獲取到當前顯示層級的控制器,並拿出它的屬性賦給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配置:

iOS 橫豎屏旋轉總結

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配置:

iOS 橫豎屏旋轉總結
在查詢螢幕旋轉相關資料的時候我發現螢幕旋轉時會最後呼叫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 指出這個問題

iOS 橫豎屏旋轉總結

這裡我確實欠考慮。順便討論一下我開始想寫但沒寫問題:

a. 橫屏跳豎屏

b. 豎屏跳橫屏
複製程式碼

下面分情況來討論:

  • 1.如果是 Present 模態切換

對於 ab 兩種情況,我們都可以直接重寫進入的控制器 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

Github ScreenRotationDemo

參考:

iOS實錄11:程式碼處理iOS的橫豎屏旋轉

How to force view controller orientation in iOS 8?


相關文章