画面の回転を検知するSwiftUIビュー

#この投稿は何？
iPhoneまたはiPadのSwiftUIアプリ開発において、デバイスの姿勢変化を検出するための方法を実践します。

# UIDeviceOrientation
`orientation`プロパティは、以下の定数を用いてデバイスの向きを識別します。
以下の定数は、あくまでも「デバイスが物理的に向いている方向」を識別するものであり、起動中のアプリケーションUIの向きとは関係ありません。

- unknown
デバイスの向きが特定できない状態。

- portrait
デバイスを縦に（ホームボタンは下側に）した状態。

- portraitUpsideDown
デバイスを縦にして、ホームボタンは上部にしたポートレートモード。

- landscapeLeft
デバイスを縦にして、ホームボタンは右側にしたランドスケープモード。

- landscapeRight
デバイスを縦にして、ホームボタンは左側にしたランドスケープモード。

- faceUp
デバイスを水平にして、画面を上向きにした状態。

- faceDown
デバイスを水平にして、画面を下向きにした状態。

`UIDeviceOrientation`列挙型について、詳しい情報は[こちらの開発者サイト](https://developer.apple.com/documentation/uikit/uideviceorientation)を確認してください。

# NotificationCenter（通知センター）
[通知センター](https://developer.apple.com/documentation/foundation/notificationcenter)は、発生した通知を集めて管理する仕組みです。

オブジェクトは`addObserver(forName:object:queue:using:)`メソッドを使用して、通知（`NSNotification`オブジェクト）を受け取るように通知センターに登録されます。オブジェクトは、自分自身をオブザーバーとして追加するときに「どの通知を受け取るか」を指定します。このメソッドを何度も呼び出せば、オブジェクトは「いくつかの異なる通知を受け取るオブザーバー」として自分自身を登録することができます。
実行中の各アプリに「デフォルトの通知センター」が存在し、通知を配信できるのは単一のプログラム内に限られます。

## addObserver(forName:object:queue:using:)メソッド
このメソッドは、通知センターに「受け取りたい通知のエントリ」を追加します。

### パラメータ

- name
オブザーバー・ブロックに配信したい通知の名称です。
ここで指定された名称の通知のみが配信されます。
`nil`の場合、通知センターは配信の基準に通知名を使用しません。

- obj
オブザーバー・ブロックに通知を送信するオブジェクトです。
ここで指定された送信者からの通知だけを配信します。
`nil`の場合、通知センターは配信の基準に送信者を使用しません。

- queue
ブロックが実行される操作キューです。
`nil`の場合、ブロックはポストしたスレッド上で同期的に実行されます。

- block
通知受信時に実行される手続きです。
通知センターはこのブロックをコピーして、オブザーバ登録が解除されるまで強い参照で保持します。
このブロックの引数は「受信した通知」です。

### 返り値
オブザーバーとして動作する不明瞭オブジェクトです。
通知センターは、オブザーバの登録を解除するまでこの返り値を強い参照で保持します。

`addObserver(forName:object:queue:using:)`メソッドについて、詳しい情報は[こちらの開発者サイト](https://developer.apple.com/documentation/foundation/notificationcenter/1411723-addobserver)を確認してください。

# UIDeviceクラス
このクラスのインスタンスは、実際にアプリケーションが起動しているデバイス自体を示します。

`UIDevice`オブジェクトを使用して、デバイスに関する情報（デバイス名、モデル名、オペレーティングシステム名とバージョンなど）を取得します。また、`UIDevice`型インスタンスを使用して、デバイスの物理的な向きなどの変化を検出します。`orientation`プロパティを使用して現在の向きを取得するか、`orientationDidChangeNotification`を通知エントリとして登録することよって変更の通知を受け取ります。
これらの方法のいずれかを使用して向き情報を取得する前に、`beginGeneratingDeviceOrientationNotifications()`メソッドを使用して通知の配信を有効にする必要があります。
デバイスの向きを追跡する必要がなくなったら、`endGeneratingDeviceOrientationNotifications()`メソッドを呼び出して通知の配信を無効化します。

`UIDevice`クラスについて、詳しくは[こちらの開発者サイト](https://developer.apple.com/documentation/uikit/uidevice)を確認してください。

## beginGeneratingDeviceOrientationNotifications()メソッド
このメソッドは、受信側が「デバイス方向の情報」を取得しようとする前に呼び出す必要があります。このメソッドは、デバイスのハードウェア機能である加速度センサーを有効にし、受信側への加速度イベントの配信を開始します。その後、受信側はこれらのイベントを使用して、デバイスの向きが変化したときに `orientationDidChangeNotification`通知を送信し、`orientation`プロパティを更新します。

このメソッドの呼び出しは `endGeneratingDeviceOrientationNotifications()`メソッドと必ず対応づけて呼び出す必要があります。

詳しくは[こちらの開発者サイト](https://developer.apple.com/documentation/uikit/uidevice/1620041-begingeneratingdeviceorientation)を確認してください。

## endGeneratingDeviceOrientationNotifications()メソッド
このメソッドは、`orientationDidChangeNotification`の通知ポストを停止し、他の場所で使用されていない場合は加速度センサーのハードウェア電源をカットできることをシステムに通知します。このメソッドを呼び出すのは、以前に`beginGeneratingDeviceOrientationNotifications()`メソッドを呼び出した後です。

詳しくは[こちらの開発者サイト](https://developer.apple.com/documentation/uikit/uidevice/1620033-endgeneratingdeviceorientationno)を確認してください。

# 実装

## OrientationDetecterクラス
このアプリケーションのデータモデルです。
クラスの宣言において`ObservableObject`プロトコルを採用しており、すべてのビュー階層から一貫性のあるデータ（信頼できる情報源）としてアクセスできます。

```swift:OrientationDetector.swift
import SwiftUI

class OrientationDetector: ObservableObject {
    @Published var currentDeviceOrientation: String = ""
    private var orientationObserver: NSObjectProtocol? = nil
    let notification = UIDevice.orientationDidChangeNotification
    
    func start() {
        UIDevice.current.beginGeneratingDeviceOrientationNotifications()
        orientationObserver = NotificationCenter.default.addObserver(forName: notification, object: nil, queue: .main) {
            [weak self] _ in
            switch UIDevice.current.orientation {
            case .faceUp:
                self?.currentDeviceOrientation = "Face Up"
            case .faceDown:
                self?.currentDeviceOrientation = "Face Down"
            case .portrait:
                self?.currentDeviceOrientation = "Portrait"
            case .portraitUpsideDown:
                self?.currentDeviceOrientation = "Portrait Upside Down"
            case .landscapeRight:
                self?.currentDeviceOrientation = "Landscape Right"
            case .landscapeLeft:
                self?.currentDeviceOrientation = "Landscape Left"
            case .unknown:
                self?.currentDeviceOrientation = "Unknown"
            default:
                break            }
        }
    }
    
    func stop() {
        if let orientationObserver = orientationObserver {
            NotificationCenter.default.removeObserver(orientationObserver, name: notification, object: nil)
        }
        orientationObserver = nil
        UIDevice.current.endGeneratingDeviceOrientationNotifications()
    }
    
    deinit {
        stop()
    }
}
```

- start()メソッド
「デバイスの物理的な向きの変化」の検出を開始するための手続きです。

- stop()メソッド
「デバイスの物理的な向きの変化」の検出を停止するための手続きです。

## OrientationDetecterApp構造体

データモデルの`OrientationDetecter`クラスを「信頼できる情報源」として初期化するために、`@StateObject`属性をマークしています。

```swift:
import SwiftUI

@main
struct OrientationDetecterApp: App {
    
    @StateObject private var detecter = OrientationDetector()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(detecter)
        }
    }
}
```

## ContentView構造体
アプリの画面となるビューです。
現在の「デバイスの物理的な向き」をテキスト表示します。

```swift:
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var detecter: OrientationDetector
    
    var body: some View {
        Text("\(detecter.currentDeviceOrientation)")
            .font(.title)
            .onAppear(perform: detecter.start)
            .onDisappear(perform: detecter.stop)
    }
}
```

`detecter`プロパティは環境オブジェクトとして宣言されています。
したがって、この値に依存しているテキストビューは、プロパティ値の変化に応じて、表示が自動的に更新されます。