【iOS】特定の画面だけ回転を許容するには

## 概要
全ての画面で回転させない, 全ての画面で回転を許容, 特定の画面だけ回転させないのは簡単にできるのですが、**特定の画面だけ回転を許容させる**のは結構厄介です。
この記事ではいくつか方法を載せますので、その時々でベストな方法を取ればよいかなと思います。

## 1. 愚直にやる場合
一番オーソドックスなやり方です。
Generalの設定で

1. Portrait
2. Landscape Left
3. Landscape Right
4. 必要であれば、Upside Down

にチェックをいれ、全体の設定で回転を許容させます。
その後、回転させたい画面以外の全てのViewController内で、以下のようにして縦画面のみ許容するようにします。

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

上記の手順で、特定の画面だけを回転可能にすることができますが、いくつかの課題も残ります。
まず、影響範囲が広いことです。開発初期段階であれば気にしないでいいかもしれませんが、既に数十画面ある大きめの既存アプリでこの対応を行う場合、回転させたい画面以外の挙動をチェックしないといけないので大変です。
また、今後新たに画面を作る場合、忘れずに縦画面だけを許容させるようにしなければなりません。
もしBaseViewControllerのような共通のクラスがある場合は、そこで指定するようにすると変更は小さく済むと思います。

## 2. 対象の画面だけで処理を完結させたい場合
先に断っておくと、この方法は非公開プロパティを触るのでiOSのバージョンによっては使えなかったり、審査でリジェクトされる可能性があります。
1の方法では、回転させたくない画面にも全て影響が出てしまいますが、この方法は回転させたい画面だけで完結します。

まず、AppDelegate内で以下を実装します。

```swift
    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        if let rootViewController = UIApplication.getTopViewController() {
            // HogeViewControllerだけは回転を許容
            if rootViewController is HogeViewController {
                return .allButUpsideDown
            }
        }
        // それ以外は縦画面のみ
        return .portrait
    }
```
上記コード内で使っている、最前面のViewControllerを取得するメソッドを作成します。

```swift
extension UIApplication {

    /// 最上位に表示されているViewControllerを取得
    static func getTopViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        
        if let nav = base as? UINavigationController {
            return getTopViewController(base: nav.visibleViewController)
            
        } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
            return getTopViewController(base: selected)
            
        } else if let presented = base?.presentedViewController {
            return getTopViewController(base: presented)
        }
        
        return base
    }
}
```

上記の実装をすることで、対象の画面だけで回転を許容できるようにできます。
しかし、これらだけでは回転中に別の画面に遷移した際に、別の画面も回転された状態で表示されてしまいます。
そのため、以下のようにして遷移時に無理矢理画面の回転を戻してあげる必要があります。

```swift
class HogeViewController {
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
    
        if self.isMovingFromParentViewController {
            UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
        }
    }
}
```

`UIDevice.current.setValue(回転方向, forKey: "orientation")` で無理矢理回転させる手法は有名ですが、非公開のプロパティにアクセスしているので正規の手段とは言えません。

## 3. 遷移先の画面で回転を戻す
2の方法では影響範囲は小さくなったものの、回転を戻す処理がいささか黒魔術的です。
そこで、回転を戻す処理を遷移先の画面で行うようにします。
例えば、

FirstViewController -> HogeViewController -> FugaViewController


という遷移が起こりうる場合、FirstVCとFugaVCで明示的に回転方向を指定すれば良いです。

```swift
class FirstViewController {
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .portrait }
}

class FugaViewController {
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .portrait }
}
```

しかし、この方法を使った場合、HogeVCから新たに導線を作る場合と、HogeVCへの新たな導線が作られた場合はそちらでも同様に回転方向を固定させる必要があります。
1の方法と比べ、影響範囲は減りましたが、結局のところ今後の実装は気をつける必要がありそうです。

## 他にはどんな方法があるのか
[monoさんのスライド](https://www.slideshare.net/mono0926/ss-48089089)では、アスペクト指向を用いた方法で実装しているようで、やはり色々やり方はありそうです。
結局、後から回転制御を入れようとすると大変なので、可能であれば初期からこのあたりを考慮するようにするのがベストかなと思います。

おわり