SwiftUIでもSize Classで縦横のレイアウトを切り替える

## はじめに
今さらながら、初めてSwiftUIで新規アプリを開発する機会があり、
縦向きと横向きでレイアウトを変える必要があったのでその方法について書きます(`・ω・´)

## サイズクラスについて
画面サイズは、高さと幅（height and width）とサイズ（regular and compact）の組み合わせで定義されます。

ざっくりと以下のような感じです。

| width | height | デバイスの種類と向き |
|:--:|:--:|:--:|
|regular|regular|iPadの縦向き・横向き|
|compact|regular|iPhoneの縦向き|
|regular|compact|大きいiPhone（Plus / Max / XR and 11など）の横向き|
|compact|compact|iPhoneの横向き|

ここでは詳細は割愛しますが、[HIGのAdaptivity and Layout](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/) の「Size Classes」のところに詳細書いてあります(￣･ω･￣)

## SwiftUIのhorizontalSizeClass, verticalSizeClassの定義について
以下のようにEnvironmentValuesにあらかじめ定義されている
horizontalSizeClass, verticalSizeClassを使用することでサイズクラスを取得できます。

```swift:SwiftUI
@available(iOS 13.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension EnvironmentValues {

    /// The horizontal size class of this environment.
    @available(macOS, unavailable)
    @available(tvOS, unavailable)
    @available(watchOS, unavailable)
    public var horizontalSizeClass: UserInterfaceSizeClass?

    /// The vertical size class of this environment.
    @available(macOS, unavailable)
    @available(tvOS, unavailable)
    @available(watchOS, unavailable)
    public var verticalSizeClass: UserInterfaceSizeClass?
}
```

↑ UserInterfaceSizeClassは、`compact`と`regular`のケースを持つenumです。

## SwiftUIのViewでサイズクラスを考慮してレイアウトを変更する
ここからが本題です(￣ω￣)
サイズクラスを取得して、iPhoneで横向きの場合はレイアウトを変えてみます。

### まずは、何も考慮しない場合の画面

ボタン3つとイメージ1つ、その下にまたボタンが1つある以下にもサンプルな画面です。
（いらすとやにONE PIECEのイラストが追加されていて驚きました...！）

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {

    var body: some View {
        VStack {
            VStack(alignment: .center, spacing: 16.0, content: {
                self.roundedButton(title: "ぼたん1", backgroundColor: .red) {
                    print("ぼたん1タップ！")
                }
                self.roundedButton(title: "ぼたん2", backgroundColor: .yellow) {
                    print("ぼたん2タップ！")
                }
                self.roundedButton(title: "ぼたん3", backgroundColor: .blue) {
                    print("ぼたん3タップ！")
                }
            })
            .padding([.top], 16.0)
            .padding([.leading, .trailing], 32.0)

            Spacer()

            Image("enel")
                .resizable()
                .frame(minWidth: 200, minHeight: 200)
                .aspectRatio(contentMode: .fit)

            Spacer()

            self.roundedButton(title: "ぼとむぼたん", backgroundColor: .orange) {
                print("ぼとむぼたんタップ！")
            }
            .padding([.top], 0)
            .padding([.leading, .bottom, .trailing], 32.0)
        }
    }
}

private extension ContentView {

    /// 角丸のボタン
    func roundedButton(title: String, textColor: Color = .white, backgroundColor: Color,  action: @escaping() -> Void) -> some View {
        Button(action: {
            action()
        }, label: {
            Text(title)
                .frame(maxWidth: 440.0, minHeight: 44.0)
                .font(Font.subheadline.weight(.bold))
                .foregroundColor(textColor)
                .background(backgroundColor)
                .cornerRadius(8.0)
        })
    }
}
```

|縦向き|横向き|
|:--:|:--:|
|![portrait_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/35b1d941-e251-9744-3912-279decda30a8.png)|![landscape_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/7ac832eb-f852-4ebb-57a1-badc511fe969.png)|

エネルのイメージの最低サイズが決まっているので、上下のボタンが見切れてしまいました(´･ω･`)

### サイズクラスを考慮して、横向きの画面のレイアウトを変更する

horizontalSizeClass, verticalSizeClassを取得するために、SwiftUIのViewにEnvironmentを追加します。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {

    @Environment(\.horizontalSizeClass) var hSizeClass // 追加1
    @Environment(\.verticalSizeClass) var vSizeClass   // 追加2
    
    var body: some View {
        // ...
    }
}
```

毎回if文を作るのはしんどそうなので、horizontalSizeClassとverticalSizeClassでイニシャライズするenumを定義します

```swift:DeviceTraitStatus.swift
import SwiftUI

enum DeviceTraitStatus {
    case wRhR
    case wChR
    case wRhC
    case wChC
    
    init(hSizeClass: UserInterfaceSizeClass?, vSizeClass: UserInterfaceSizeClass?) {
        
        switch (hSizeClass, vSizeClass) {
        case (.regular, .regular):
            self = .wRhR
        case (.compact, .regular):
            self = .wChR
        case (.regular, .compact):
            self = .wRhC
        case (.compact, .compact):
            self = .wChC
        default:
            self = .wChR
        }
    }
}
```

先ほどの画面を修正していきます。
横向きの場合は、赤いボタンと黄色いボタンを横並びにして画面に収まるようにしてみます！

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {

    @Environment(\.horizontalSizeClass) var hSizeClass
    @Environment(\.verticalSizeClass) var vSizeClass
    
    var body: some View {
        VStack {
            let deviceTraitStatus = DeviceTraitStatus(hSizeClass: self.hSizeClass, vSizeClass: self.vSizeClass)
            switch deviceTraitStatus {
            case .wRhR, .wChR:
                self.buttonsOnPortrait
            case .wRhC, .wChC:
                self.buttonsOnLandscape
            }

            // ...
        }
    }
}

private extension ContentView {

    var button1: some View {
        self.roundedButton(title: "ぼたん1", backgroundColor: .red) {
            print("ぼたん1タップ！")
        }
    }

    var button2: some View {
        self.roundedButton(title: "ぼたん2", backgroundColor: .yellow) {
            print("ぼたん2タップ！")
        }
    }

    var button3: some View {
        self.roundedButton(title: "ぼたん3", backgroundColor: .blue) {
            print("ぼたん3タップ！")
        }
    }

    /// iPhoneの縦表示またはiPadの場合のボタン3つのレイアウト
    ///
    /// - Note: iPadの場合は、縦横ともに画面に収まるのでレイアウト変えない。
    var buttonsOnPortrait: some View {
        VStack(alignment: .center, spacing: 16.0, content: {
            self.button1
            self.button2
            self.button3
        })
        .padding([.top], 16.0)
        .padding([.leading, .trailing], 32.0)
    }

    /// iPhoneの横表示の場合のボタン3つのレイアウト
    var buttonsOnLandscape: some View {
        VStack {
            HStack {
                self.button1
                Spacer()
                self.button2

            }
            .padding([.leading, .trailing], 32.0)
            .padding([.bottom], 4.0)

            self.button3
        }
        .padding([.top], 16.0)
    }

    /// 角丸のボタン
    func roundedButton(title: String, textColor: Color = .white, backgroundColor: Color,  action: @escaping() -> Void) -> some View {
        // ...
    }
}
```

|修正後の縦向き|修正後の横向き|
|:--:|:--:|
|![portrait_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/0e2af4e1-20d7-c040-9dc9-d325d9196616.png)|![landscape_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/cacf355a-c7d1-4453-479e-a2fd8397e7f2.png)|

画面回転したら、レイアウトが変更されるようになりました\\(｡･ω･｡)/

## さいごに
SwiftUIはまだまだ勉強中なので、もっと良いやり方などあればコメントをお願いしますm(_ _)m

ソースコードは、GitHubに[リポジトリ](https://github.com/y-okudera/SwiftUIDeviceTraitStatus)を作ってpushしました。