この記事は何?
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
ゲーム中のスコアを追跡するアプリを作成し、カスタム型がデータモデリングを容易にする方法を確認します。
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をイテレートします。
$プレフィックスを使用すると、配列内の各プレイヤーにバインディングできます。
PlayerはIdentifiableに適合させてあるので、、ForEachのid:パラメータを指定する必要はなくなりました。
新しい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を整理して、「プレイヤーの名前とスコア」を列とヘッダーに並べます。
ForEachをGridビューでラップします。
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アプリのコアロジックを表す別のカスタム型を定義します。
また、アプリが意図したとおりに機能することを証明するコードも作成します。
記事の続きは、こちら。
