2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Udemyおすすめ講座をシェアしよう! by UdemyAdvent Calendar 2024

Day 4

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

Last updated at Posted at 2024-12-04

この記事は何?

Develop in Swiftに追加された新しいチュートリアル「Custom types and Swift Testing」をやってみたので、備忘録として残しておきます。

Model data with custom types」では、現実世界の概念をコードのモデルとして表現する方法を学びます。
さまざまなテクニックを駆使して、独自のSwift型でデータモデルを作成し、アプリを強化します。

Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。

What you'll learn

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

About data modeling

データモデリングは、現実世界の概念をコードのモデルとして表現する手法です。
これについては、以前のチュートリアルでいくつかを経験済みです。
状態のあるUIの更新でサイコロを振るとき、単一の数字を使用してサイコロをモデル化しました。
同様に、動的コンテンツの作成で仲間のグループを文字列の配列としてモデル化しました。

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

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

What you'll need

  • SwiftUI の基礎でカバーされているXcode、Swift、およびSwiftUIの基礎に精通している
  • macOS Sequoiaか、Sonomaを実行しているMac

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

Model data with custom types

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

スクリーンショット 2024-12-04 12.41.49.png

1. Create an app with a list of players

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

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を追加することで、クラッシュを修正できます。
プレビューの追加ボタンをクリックして、クラッシュが修正されたことを確認します。
このアプローチでプレイヤーをモデル化すると、クラッシュが繰り返し発生する問題を発見しました。
データに矛盾が生じて、一貫性がなくなりがちです。

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が一意であることを保証できます。

import Foundation

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

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

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

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

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

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

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を整理して、「プレイヤーの名前とスコア」を列とヘッダーに並べます。

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

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

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

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

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アプリのコアロジックを表す別のカスタム型を定義します。
また、アプリが意図したとおりに機能することを証明するコードも作成します。

記事の続きは、こちら

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?