【iOS】縦画面と横画面でレイアウト切替えが必要な時に使えるTips

# はじめに
今回は、アプリを回転したときに「縦レイアウト」と「横レイアウト」を動的に別のレイアウトにしたいという要件を満たす必要があるときに、僕がよく実装で使っているやり方を記事にしてみました。

おそらく、王道のやり方としては、回転した時に制約を変更して、レイアウトを切り替えるのだと思うのですが、それをやるのが不便な時によく使っている方法になります。

# こんなときに使える
今回記事の中で実装している、例を上げると以下の画像のような感じです。
縦のときは、縦1列に並んでいるレイアウトを採用していますが、横のときは、左側に大きく表示するViewと右側に２つ並んでいるViewに分かれています。
**縦レイアウト**
<img width="200" alt="vertical.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/f599553b-9434-5a70-ea15-5ada9cee4169.png">
**横レイアウト**
<img width="360" alt="horizon.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/a4c9bb8b-1ecf-2b50-04dc-869c63221bbd.png">

今回は、Viewに`addSubView`する対象が、①〜③のラベルですが、`TableView`や自分で作成したカスタムViewを`addSubView`することが多いと思います。

# 実装方法

### StoryBoardの設定
**縦レイアウト**
![verical_storyboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/7f5269c7-84e9-2840-82a3-7a20c8d3777a.png)
**横レイアウト**
![horizon_storyboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/b4dac5f5-43c9-f3f1-123f-7ab2ab7eb5e0.png)

表示したいView（今回はLabel）を乗せるための親Viewを、縦と横で別々で用意します。これらの親Viewは、ソースコードと`@IBOutlet`接続する必要があります。

StoryBoardを編集する時、編集しない親Viewについては、表示されていると邪魔であるため、「installed」のチェックを外すのがおすすめです。
![installed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/c872f76d-f892-ecfc-3da9-699443945c98.png)
※ビルドするときはチェックを付けてください。


### ソースコード
```swift:ViewRotationViewController.swift
import UIKit

class ViewRotationViewController: UIViewController {
    
    @IBOutlet weak private var viewFirst: UIView!
    @IBOutlet weak private var viewSecond: UIView!
    @IBOutlet weak private var viewThird: UIView!
    
    @IBOutlet weak var viewFirstHorizon: UIView!
    @IBOutlet weak var viewSecondHorizon: UIView!
    @IBOutlet weak var viewThirdHorizon: UIView!
    
    @IBOutlet weak var viewVertical: UIView!
    @IBOutlet weak var viewHorizon: UIView!
    
    // 子Viewとして表示するラベル（実際にアプリを作るとなると、カスタムViewやTableViewになることが多い）
    private let labelNo1 = UILabel()
    private let labelNo2 = UILabel()
    private let labelNo3 = UILabel()
    
    private var isPortrait: Bool {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isPortrait ?? true
    }
    
    override func viewDidLoad() {
        initLabel()
        setScreen()
    }
    
    private func initLabel() {
        labelNo1.text = "①"
        labelNo2.text = "②"
        labelNo3.text = "③"
        
        labelNo1.textAlignment = .center
        labelNo2.textAlignment = .center
        labelNo3.textAlignment = .center
        
        labelNo1.font = .systemFont(ofSize: 60)
        labelNo2.font = .systemFont(ofSize: 60)
        labelNo3.font = .systemFont(ofSize: 60)
    }
    
    private func setScreen() {
        labelNo1.removeFromSuperview()
        labelNo2.removeFromSuperview()
        labelNo3.removeFromSuperview()
        
        if isPortrait {
            viewFirst.addSubViewFill(labelNo1)
            viewSecond.addSubViewFill(labelNo2)
            viewThird.addSubViewFill(labelNo3)
        } else {
            viewFirstHorizon.addSubViewFill(labelNo1)
            viewSecondHorizon.addSubViewFill(labelNo2)
            viewThirdHorizon.addSubViewFill(labelNo3)
        }
        // 縦Viewと横Viewの表示・非表示を切り替える
        viewVertical.isHidden = !isPortrait
        viewHorizon.isHidden = isPortrait
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate { [weak self] _ in
            guard let `self` = self else { return }
            // 回転時に、レイアウトの変更を行う
            self.setScreen()
        }
    }
}
```

`addSubViewFill`というメソッドについては、画面いっぱいに子Viewを貼り付けるために、UIViewをExtensionしました。
詳しくはこちらの記事を見てもらえればと思います。
https://qiita.com/lemonade_dot_log/items/5304c0fc3fb82adcf1e5

```swift:UIView+Extension.swift
extension UIView {
    func addSubViewFill(_ childView: UIView) {
        self.addSubview(childView)
        childView.translatesAutoresizingMaskIntoConstraints = false
        childView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
        childView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
        childView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
        childView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
    }
}
```

### 解説
`viewDidLoad()`（初回表示時）、`viewWillTransition()`（回転した時）で現在のアプリの画面向きを判断して、それによってレイアウトを切り替える方法を採用しています。(`setScreen()`を呼び出す。)

ポイントとしては、`setScreen()`で、画面の向きによって、表示する子Viewを乗せる親Viewを変更している点です。
回転時に`setScreen()`を呼び出すことで、子Viewの親Viewを変更し、レイアウトを変更することが可能になっています。

流れとしては、以下のような流れです。
「アプリが回転」→「子Viewを親Viweから切り離す」→「縦か横かを判断」→「適切な親Viewに子ViewをaddSubView()する」

こうすることで、親ViewのAutoLayout制約を切り替える方法ではなく、子Viewの切り離し&貼り付けによって、レイアウト変更を実現しているという感じです。
イメージとしては、のりで1度貼った紙を、剥がしてから別の台紙に貼り付けるといった感じでしょうか。

# おわりに
レイアウト切り替えで他におすすめの方法があったら、是非コメント欄で教えていただけると嬉しいです。
あとは、SizeClassにレイアウトを完全に任せる方法があると思いますが、iPadは縦と横のSizeClassが同じなので、今回は採用しませんでした。