画面ごとにオリエンテーションを設定する方法

畑田です。
ある開発においてview controllerごとにorientationを細かく設定する機会があったので記録に残します。   
全てコードで書いています。   

## 環境
Swift version 5.2.4   
Xcode version 11.6   

## fix application orientation
アプリ全体で画面の向きを固定する場合は、`Info.plist`の`Supported interface orientations`において設定します。   
例えば`Supported interface orientations`を`Portrait (bottom home button)`のみにすると、縦画面のみがサポートされますし、`Portrait (bottom home button)`、`Portrait (top home button)`、`Landscape (left home button)`、`Landscape (right home button)`にすると全ての画面の向きがサポートされます。   

## fix launch screen orientation
launch screenの画面の向きはapplicationがlaunchされる前の設定なので、コードで設定するというよりは`Info.plist`の`Supported interface orientations`において設定します。   
ただし、こうしてしまうとアプリ全体がそのorientationに固定されてしまうので、設定しなおしてあげる必要があります。   
これは`AppDelegate.swift`に記述してあげます。以下を参照してください。   

```swift:AppDelegate.swift
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.allButUpsideDown
}
```
こうしてあげると、launch後の画面はXcodeのデフォルトの設定(上下反対の縦画面以外をサポートする設定)に戻ります。   

## fix orientation of each view controller
view controllerごとの画面の向きの設定は`UIViewController`の`shouldAutorotate`と`supportedInterfaceOrientation`をoverrideしてあげることで実現します。   

```swift:EachViewController.swift
override var shouldAutorotate: Bool {
    return false
}

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.portrait
}
```

## fix orientation of navigation controller or tab bar controller
navigation controllerなどを使っている場合は、そのorientationを`visibleViewController`のorientationに従わせるようなextensionを追加することで実現できます。   

```swift:UINavigationController+Extension.swift
import UIKit

extension UINavigationController {
    open override var shouldAutorotate: Bool {
        guard let viewController = self.visibleViewController else { return true }
        return viewController.shouldAutorotate
    }

    open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        guard let viewController = self.visibleViewController else { return .all }
        return viewController.supportedInterfaceOrientations
    }
}
```

## set orientation when the view appears
それぞれのview controllerが立ち上がったときに画面の向きを指定したい場合は、以下のようなメソッドで実現可能です。   
これを画面の向きを強制しタイミングで呼び出してあげるだけです。   
もちろん`.portrait`の部分を変えてあげれば他のorientationにも対応できます。

```swift:EachViewController.swift
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
```
ただし、このメソッドは強制的に画面の向きを指定したように回転させるので、呼び出したときに`shouldAutorote` propertyが`true`になっていないとエラーが起こります。   
しかも、このメソッドは呼び出すたびに一度だけ強制的に画面を回転させるだけで、その後の画面の向きの保持には関わりません。   
したがって、`shouldAutorotate`の値をflagで管理し、以下のようにするのが自分なりのpracticeです。

```swift:EachViewController.swift
class EachViewController: UIViewController {
    
    // flag
    private var canRotate = true

    override var shouldAutorotate: Bool {
        return canRotate
    }

    override func viewDidLoad() { // viewDidLayoutSubviews()などでも
        super.viewDidLoad()

        // free orientation
        canRotate = true

        // force orientation change
        UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")

        // fix orientation
        canRotate = false

        // skip details
    }

}
```