【SwiftUI】アプリケーションのモデルデータを管理する

#この記事は何か
SwiftUIフレームワークは、アプリのデータモデルとビューを接続します。
この投稿では、SwiftUIアプリの「モデルのデータを管理する方法」について、[Apple Developerサイトのリファレンス](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)を独自に解釈・翻訳します。

###環境
- macOS 11.0.1
- Xcode 12.2
- Swift 5.3

#概要
通常、アプリ内でデータを保存したり処理するには、アプリのUIやその他のロジックとは分離されたデータモデルを使用します。データモデルを分離することでモジュール化が促進され、テストしやすくなり、アプリがどのように動作するかがわかりやすくなります。

従来はビューコントローラを使用して、モデルとUIの間でデータをやり取りしていました。SwiftUIは、このやり取りのほとんどを自動的に処理してくれます。データの変更に合わせてビューを更新するには、データモデルのクラスを**観測可能なオブジェクト**にして、そのプロパティを**パブリッシュ（公開）**します。そして、そのインスタンスを宣言する際に「特殊な属性」を使用します。また、「ユーザーによって行われたデータの変更」を確実にモデルに反映するには、UIコントロールをモデルのプロパティに**バインド**します。これらの機能を併用することで、データを**Source of Truth（信頼できる情報源）**にできます。

##観測可能なモデルデータを作成する
モデルデータにおける変更をSwiftUIに感知させるには、モデルとなるクラスに`ObservableObject`プロトコルを採用します。
次のコードは、観測可能なオブジェクトとして`Book`クラスを作成します。

````Swift:観測可能なBookクラスを作成する
class Book: ObservableObject {
}
````

システムは、クラスの付属型`ObjectWillChangePublisher`型を自動的に推論します。そして、自動的に合成されるプロトコル要件の`objectWillChange`メソッドによって、`@Published`属性プロパティの変更を発信します。プロパティをパブリッシュするには、宣言に`@Published`属性をマークします。

````Swift:titleプロパティを@Published属性にして、変更を発信する
class Book: ObservableObject {
    @Published var title = "Great Expectations"
}
````

不要な公開プロパティによるオーバーヘッドは回避してください。値に変更が発生し、UIにとって重要なプロパティだけを公開します。例えば、`Book`クラスには、初期化しても変更されない識別子を示す`identifier`プロパティがあるかもしれません。

````Swift:
class Book: ObservableObject {
    @Published var title = "Great Expectations"

    let identifier = UUID() // 変更されない識別子
}
````

識別子をUIに表示することもできますが、このプロパティは`@Publised`属性ではないので、SwiftUIがidentifierプロパティの変更を監視することはありません。

##観測可能なオブジェクトにおける変更を感知する
SwiftUIに観測可能なオブジェクトを監視させるには、プロパティの宣言に`@ObservedObject`属性を追加します。

````Swift:ビューのプロパティを監視オブジェクトにする@ObservedObject属性
struct BookView: View {
    @ObservedObject var book: Book // モデルのインスタンスを監視させる
    
    var body: some View {
        Text(book.title)
    }
}

````

上記のような観測オブジェクトを示すプロパティを個別に子ビューに渡すことができます。データが変更されると、ディスクから新しいデータをロードしたかのように、SwiftUIは影響を受けるビューのみ表示を更新します。また、観測可能なオブジェクト自体を子ビューに渡して、ビュー階層のレベル間でモデルオブジェクトを共有することもできます。

````Swift:
struct BookView: View {
    @ObservedObject var book: Book
    
    var body: some View {
        BookEditView(book: book) // 子ビューを初期化する際にモデルを渡す
    }
}

// Bookビューの子ビュー
struct BookEditView: View {
    @ObservedObject var book: Book // モデルを共有

    // ...
}
````


##ビュー内のモデルオブジェクトを初期化する
SwiftUIは何度もビューを作成・再作成するので、与えられた入力値のセットで常に同じビューを初期化できることが重要です。そのため、観測オブジェクトをビュー内で作成することは危険です。その代わりに、SwiftUIは`@StateObject`属性を提供します。この属性を使用すると、ビュー内でも安全に`Book`型インスタンスを作成することができます。

````Swift:
struct LibraryView: View {
    @StateObject var book = Book()  // このライブラリビューで作成、管理される
    
    var body: some View {
        BookView(book: book)
    }
}
````

SwiftUIが「ビュー側でオブジェクトインスタンスを作成・管理される」と知っていることを除けば、`@StateObject`属性プロパティは「観測オブジェクト」と同じです。何度でも、ビューを再作成できます。上のコードのようにローカルで使用したり、あるいは「別のビュー」の`@ObservedObject`属性プロパティに渡したりできます。

SwiftUIはビュー内で`＠StateObject`属性のインスタンスを再作成しませんが、それぞれのビューに対して個別のインスタンスを作成します。以下のコードにおいて、それぞれの`LibraryView`は一意の`Book`インスタンスを取得します。

````Swift:LibraryView毎に、ユニークなBookインスタンスが作成される
VStack {
    LibraryView()
    LibraryView()
}
````

また、トップレベルの`App`インスタンス、および`Scene`インスタンスの中で`@StateObject`属性のインスタンスを作成することもできます。例えば、「ブックリーダーアプリ内で本のコレクションを保持する」ために`Library`という観測可能なオブジェクトを定義する場合、アプリのトップレベルに単一の`Library`インスタンスを作成します。

````Swift:
@main
struct BookReader: App {
    @StateObject var library = Library()

    // ...
}
````

##アプリケーション全体でオブジェクトを共有する
「アプリ全体にわたって使用したいデータモデル」のオブジェクトを何層も貫通させたくない場合は、`environmentObject(_:)`モディファイアを使用します。ビュー間で受け渡しながら共有する代わりに、モデルオブジェクトを環境に配置できます。

````Swift:アプリケーションを示すBookReader型
@main
struct BookReader: App {
    @StateObject var library = Library()
    
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environmentObject(library)
        }
    }
}
````

プロパティを`@EnvironmentObject`属性で宣言することで、モディファイアを適用したビューはその子孫にわたって、データモデルのインスタンスにアクセスできます。

````Swift:ライブラリビューは、環境オブジェクトのモデルにアクセスできる
struct LibraryView: View {
    @EnvironmentObject var library: Library
    
    // ...
}
````

上記のように、`@EnvironmentObject`属性のインスタンスは「アプリにおけるビュー階層の最上位」に作成して使用します。あるいは、ビュー階層におけるサブツリーのルートビューに作成しても良いでしょう。いずれにしても、オブジェクトを使用するビューや子孫ビューでは、「プレビュープロバイダにも追加すること」を忘れないでください。

````Swift:プレビュープロバイダに環境オブジェクトを追加する
struct LibraryView_Previews: PreviewProvider {
    static var previews: some View {
        LibraryView()
            .environmentObject(Library())
    }
}
````

##バインディングを使って、双方向の接続を作成する
UIコントロールからデータモデルを変更できるようにするには、対応するプロパティへの**バインディング**を使用します。バインディングを経由することにより、更新が自動的にデータモデルに反映されます。`@OvservedObject`属性、`@StateObject`属性、および`@EnvironmentObject`属性プロパティにバインディングするには、オブジェクト名にドル記号 `$`を付けます。例えば、ユーザーに「本のタイトル」を編集させるために`BookEditView`ビューに`TextField`コントロールを追加する場合、`TextField`と`$book.title`プロパティのバインディングを設定します。

````Swift:
struct BookEditView: View {
    @ObservedObject var book: Book
    
    var body: some View {
        TextField("Title", text: $book.title)
    }
}
````

バインディングは「ビューの要素」を「基礎となるモデル」に接続して、ユーザーがモデルデータの値を直接変更できるようにします。