【SwiftUI】データをモデル化する独自型の定義

# この記事は何？
[Develop in Swift](https://developer.apple.com/tutorials/develop-in-swift)に追加された新しいチュートリアル「Custom types and Swift Testing」をやってみたので、備忘録として残しておきます。

「[Model data with custom types](https://developer.apple.com/tutorials/develop-in-swift/model-data-with-custom-types)」では、現実世界の概念をコードのモデルとして表現する方法を学びます。
さまざまなテクニックを駆使して、独自のSwift型でデータモデルを作成し、アプリを強化します。

> Swiftを基礎から学ぶには
自著、工学社より発売中の「[まるごと分かるSwiftプログラミング](https://amzn.asia/d/7YPWJwl)」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
> また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「[今日からはじめるプログラミング](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwii1qTuj_2BAxV4hlYBHbj5BqEQFnoECBYQAQ&url=https%3A%2F%2Fwww.udemy.com%2Fcourse%2Fstart-programming-today%2F&usg=AOvVaw2vs4Sccq-bpfA4ZD2rCyNm&opi=89978449)」をお勧めします。

### What you'll learn

- Swiftで独自のデータ型をモデル化して、現実世界の概念を表現する方法
- コードが期待どおりに機能することを確認するために、[Swift Testing](https://developer.apple.com/xcode/swift-testing/)でテストを作成する方法
- [SwiftData](https://developer.apple.com/xcode/swiftdata)の基本は、想像できるあらゆる種類のデータでアプリを強化するために使用できます。
- [Xcode](https://developer.apple.com/xcode/)プレビューを使用してデータモデルに関するインターフェイスを設計する方法
- 複雑なアプリ間でデータモデルを共有および調整する方法

### About data modeling
データモデリングは、現実世界の概念をコードのモデルとして表現する手法です。
これについては、以前のチュートリアルでいくつかを経験済みです。
[状態のあるUIの更新](https://developer.apple.com/tutorials/develop-in-swift/update-the-ui-with-state)でサイコロを振るとき、単一の数字を使用してサイコロをモデル化しました。
同様に、[動的コンテンツの作成](https://developer.apple.com/tutorials/develop-in-swift/create-dynamic-content)で仲間のグループを文字列の配列としてモデル化しました。

これらのモデルはプロトタイプでは問題ありませんが、より複雑なアプリにはより複雑なデータが必要です。
複数の種類のデータをモデル化したい場合があります。
異なる種類のデータ間の関係をモデル化することもできます。
モデルをコードで表現することで、その複雑さの管理が容易になります。

各アプリは、アプリに適した方法でデータモデルを変換、表示、および変更します。
天気予報は整数を温度として扱い、ダイスローラーは整数をサイコロのパイプ柄として表示します。
ビジネスロジックとは、データが操作されるアプリ固有の方法を指します。
データモデリングにはデータ自体に加えて、ビジネスロジックが含まれます。

### What you'll need
- [SwiftUI の基礎](https://developer.apple.com/tutorials/develop-in-swift/welcome-to-develop-in-swift-tutorials)でカバーされているXcode、Swift、およびSwiftUIの基礎に精通している
- macOS Sequoiaか、Sonomaを実行しているMac

> Note:
iOSデバイスは必要ありません。
Xcodeには、Mac上でアプリの外観と動作を示すツールが含まれています。

## Model data with custom types
ゲーム中のスコアを追跡するアプリを作成し、カスタム型がデータモデリングを容易にする方法を確認します。

![スクリーンショット 2024-12-04 12.41.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/19ed8644-790a-bbd4-3fbe-6724b06e41bf.png)

### 1. Create an app with a list of players

デフォルトのプレイヤーリストを作成し、新しいプレイヤーを追加させます。
最初に、各プレイヤーを文字列としてモデル化します。
このモデルは、チュートリアルの後半で改善します。

```swift
import SwiftUI

struct ContentView: View {
    @State private var players = ["Elisha", "Andre", "Jasmine"]
    
    var body: some View {
        VStack {
            ForEach(0..<players.count, id: \.description) { index in
                TextField("Name", text: $players[index])
            }
            Button("Add Player", systemImage: "plus") {
                players.append("")
            }
        }
        .padding()
    }
}
```

### 2. Add scores
それぞれのプレイヤーごとにスコアが必要です。
これをモデル化するには、「整数が`players`配列の要素名と同じ順に並んだ配列」を作成します。
アプリを構築し続けていき、データモデルの一貫性がなくなった際のクラッシュを特定して、修正します。

プレイヤーをモデル化する文字列は名前とスコアを同時に追跡できないので、スコアを個別にモデル化しています。
今のところ、これら2つの配列の同じインデックスに2つのデータが表示されることを確認することで、各プレイヤーの名前をスコアに関連付けます。
例えば、アンドレは`players`配列のインデックス`1`なので、スコアも`scores`配列の`1`になります。

`Stepper`でも`TextField`の名前で使用しているのと同じインデックスを使用しますが、プレビューがクラッシュすることに注意してください。
これは、モデルに一貫性がないために発生します。
プレイヤー名とスコアが一致するようにインデックスを使用する配列データを2つモデル化しましたが、`players`配列には3つの要素がある一方、`scores`配列は空です。
配列にないインデックスを読み取ろうとすると、「範囲外のインデックス」エラーでクラッシュします。

各プレイヤーの`scores`配列に`0`を追加してモデルの一貫性を修正してから、プレビューを更新します。
それぞれのプレイヤーに関連する名前とスコアが出来たので、モデルは一貫しています。

プレビューで追加ボタンをクリックすると、また「範囲外のインデックス」クラッシュが発生します。
追加ボタンをクリックすると、新しいプレイヤー名の空文字列が追加されますが、一致するスコア配列には何も追加されません。
データモデルは「スコアのないプレイヤーの作成」をサポートしていません。

`players`配列に名前を追加するたびに`scores`配列に`0`を追加することで、クラッシュを修正できます。
プレビューの追加ボタンをクリックして、クラッシュが修正されたことを確認します。
このアプローチでプレイヤーをモデル化すると、クラッシュが繰り返し発生する問題を発見しました。
データに矛盾が生じて、一貫性がなくなりがちです。

```swift
import SwiftUI

struct ContentView: View {
    @State private var players = ["Elisha", "Andre", "Jasmine"]
    @State private var scores: [Int] = [0, 0, 0]
    
    var body: some View {
        VStack {
            ForEach(0..<players.count, id: \.description) { index in
                TextField("Name", text: $players[index])
                Stepper("\(scores[index])", value: $scores[index])
            }
            Button("Add Player", systemImage: "plus") {
                players.append("")
                scores.append(0)
            }
        }
        .padding()
    }
}
```

### 3. Create your own player
プレイヤー名とスコアを別個の配列でモデル化すると、データモデルの一貫性は維持しにくくなることがわかりました。
対策として、プレイヤーに関するすべてのデータをモデル化する、独自の`Player`型を作成します。
この手法によって、データは一貫性を常に維持できるようになります。

`Player.swift`ファイルを作成します。
SwiftUIファイルとSwiftファイルの違いに注意してください。
プレイヤーファイルに`body`やプレビューはありません。
`Player`構造体がデータを追跡して、`ContentView`に表示します。

`Player`型に`Identifiable`プロトコルを採用させます。
プロトコルはメソッド、プロパティ、その他の要件設計を定義します。
これらすべての要件を満たすことは、そのプロトコルに準拠することを意味します。

`id`プロパティを定義して、`Player`型を`Identifiable`プロトコルに準拠させます。
インスタンスの`id`はプレイヤー名が変更されても影響を受けないので、SwiftUIは混乱せずに、複数のプレイヤーを同じ名前にできます。
SwiftUIは`id`を使用して、モデルの変更とまったく新しいモデルの違いを識別します。
`UUID`は「全般で固有な識別子」なので、これを使用すると各インスタンスのIDが一意であることを保証できます。

```swift
import Foundation

struct Player: Identifiable {
    var id = UUID()
    var name: String
    var score: Int
}
```

コンテンツビューで、`players`配列と`scores`配列ではなく、`Player`オブジェクトの配列にリファクタリングします。

`scores`配列を削除し、`players`配列を更新して、新しい`Playger`型インスタンスを保持します。
これにより発生するいくつかのコンパイルエラーには、以降の手順で対処します。
ここでは、名前とスコアがあるプレイヤーをモデル化しました。
したがって、数ステップ前のクラッシュと同様に、名前のみでプレイヤーを作成しようとすると、エラーが発生します。

`ForEach`ループを更新して、`$players`をイテレートします。
`$`プレフィックスを使用すると、配列内の各プレイヤーにバインディングできます。
`Player`は`Identifiable`に適合させてあるので、、`ForEach`の`id:`パラメータを指定する必要はなくなりました。

新しい`Player`インスタンスを使用するには、`TextField`のバインディングを更新します。
次に、`Stepper`のバインディングを更新して、新しいプレイヤーインスタンスを使用します。

ボタンの`action:`クロージャにある2つの`append`メソッドを、`Player`インスタンス作成に置き換えます。
シミュレーターでアプリを実行します。
スコア調整やプレイヤー追加を行って、データの一貫性が常に維持できることを確認してください。

```swift
import SwiftUI

struct ContentView: View {
    @State private var players = [
        Player(name: "Elisha", score: 0),
        Player(name: "Andre", score: 0),
        Player(name: "Jasmine", score: 0),
    ]
    
    var body: some View {
        VStack(alignment: .leading) {            
            ForEach($players) { $p in
                TextField("Name", text: $p.name)
                Stepper("\(p.score)", value: $p.score)
            }        
            
            Button("Add Player", systemImage: "plus") {
                let newOne = Player(name: "", score: 0)
                players.append(newOne)
            }
        }
        .padding()
    }
}
```

### 4. Improve your app design
アプリのタイトルを追加します。
そして、`Grid`ビューでUIを整理して、「プレイヤーの名前とスコア」を列とヘッダーに並べます。

`ForEach`を`Grid`ビューでラップします。
`Grid`ビューと`GridRow`ビューは、複数行のビューを水平に揃えるのに最適です。
これらのビューは連携して、コンテンツの各行が完璧な列に整列されます。

`GridRow`ビューを使用して、`TextField`ビューと`Stepper`ビューを列にします。
`ForEach`の上に`GridRow`を追加して、プレイヤーとスコア列のタイトル列を保持します。
`.Headline`フォントを使用して、タイトルをより目立たせます。

プレイヤー列に`Text`ビューを追加してスコアを表示します。
これにより、`Stepper`のラベルは冗長になるので、`.labelsHidden()`で隠してください。
`Stepper`ビューのラベルが可能な限りのスペースを占有するのとは対照的に、`Text`ビューは必要なスペースのみを占有します。
この変更を行うと、プレイヤーのスコアの列がはるかに狭くなります。

`.gridColumnAlignment`モディファイアを`Players`ヘッダーに追加します。
プレイヤー名に合わせて、`.leading`にしてください。
`Grid`ビューに垂直方向の余白を追加します。
`VStack`の最後に`Spacer`ビューを追加して、全体を上にプッシュします。

```swift
import SwiftUI

struct ContentView: View {
    @State private var players = [
        Player(name: "Elisha", score: 0),
        Player(name: "Andre", score: 0),
        Player(name: "Jasmine", score: 0),
    ]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Score Kepper")
                .font(.title)
                .bold()
                .padding(.bottom)
            
            Grid {
                GridRow {
                    Text("Player")
                        .gridColumnAlignment(.leading)
                    Text("Score")
                }
                .font(.headline)
                
                ForEach($players) { $p in
                    GridRow {
                        TextField("Name", text: $p.name)
                        Text("\(p.score)")
                        Stepper("\(p.score)", value: $p.score).labelsHidden()
                    }
                }
            }
            .padding(.vertical)
            
            Button("Add Player", systemImage: "plus") {
                let newOne = Player(name: "", score: 0)
                players.append(newOne)
            }
            
            Spacer()
        }
        .padding()
    }
}
```


## What’s next?
チュートリアルの続きでは、ScoreKeeperアプリのコアロジックを表す別のカスタム型を定義します。
また、アプリが意図したとおりに機能することを証明するコードも作成します。

記事の続きは、[こちら](https://qiita.com/imchino/items/b24453537d5c2e972fc5)。