概要:
iPadのアプリSwift Playgroundsの「グリッドを使った整理」のソースコードで、
ForEachを中心にプロトコル、Hashable、id: \.selfなどをわかり易く解説します。
「グリッドを使った整理」は以下でも解説していますので、合わせてご覧ください。
ForEach:SwiftUIの要
SwiftUIを学び始めて最初の壁がこのForEachでした(笑)
いくつか難しい概念の理解が必要ですが、最初は感覚的に理解していきましょう。
ContentView.swiftファイルのソースコードのForEachの部分を構造的に簡略化すると以下のようになります。
LazyVGrid(columns: columnLayout) {
ForEach(allColors.indices, id: \.self) { index in
// Buttonの処理
}
}
LazyVGridの中に13個(allColors構造体の要素は13)のボタンを作って表示するのですが、13個を 1個ずつループで回しているのがForEachの役割になります。
Forで、Eachなんだから、それぞれをループで回すだけでしょ?
For Loopでいいんじゃない?
と思って、次のようにfor in で書き換えてみると、、
LazyVGrid(columns: columnLayout) {
for index in 0..<allColors.count { // 書き換えた箇所
// Buttonの処理
}
}
”Closure containing control flow statement cannot be used with result builder ‘ViewBuilder’”というエラーが思いっきり出ました(笑)
「グリッドを使った整理」というアプリのソースコードを解説するところからどんどんズレていくので多くは語りませんが、、
表示を担当するViewの基本部分では、for-in loopのようなcontrol flow statementを含むクロージャー(ここではLazyVGridの{}の中)は使えないということです。
ということで今回は、
こういう場所では基本的にForEachを使う
と理解してください(汗)。(ViewBuilderの話は別な機会に)
ということでForEachに戻ります。
ForEachの基本的な動作
もう一度次のコード部分に戻ります。
ForEach(allColors.indices, id: \.self) { index in
// Buttonの処理
}
id: \.selfの部分は後で説明しますので、とりあえず無視してもらって考えると次のようになります
ForEach( 値を複数取りえる配列など ) { 順に1つの値 in
その1つの値を使って行う処理
}
実験してみましょう。
以下のように、どれかのブロックが押された時にallColors.indicesとindexをprintで表示するようにしてみました。
ForEach(allColors.indices, id: \.self) { index in
Button {
selectedColor = allColors[index]
print("allColors.indices: \(allColors.indices), index: \(index)") // ←ここ
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(allColors[index])
}
.buttonStyle(.plain)
}
左上から下へ3つ四角ボタンを押してみました。print結果は以下です。
allColors.indices: 0..<13, index: 0
allColors.indices: 0..<13, index: 4
allColors.indices: 0..<13, index: 8
allColors.indicesには0..<13と常に表示されています。つまりこのindicesは0から12の13個の整数を持っているということになります。
indexにはその時の押された四角ボタンの番号(indicesのどれか)が入っています。
ForEachは単に表示をループで回すだけかと思っていましたが、ボタンを押された時にそのindexをセットして呼ばれてくるのですね。
以下の行で記述しているように、押された四角ボタンの色をタイトルの文字の色に設定しています。
selectedColor = allColors[index]
ForEachの理解を深める
上の例を見ていて、
allColorsはColorの配列なんだから、Colorで取り出せるのでは?
と思った方、さすがです。
ForEachをここでマスターしてしまうために、早速書き換えてみましょう。
ForEach(allColors, id: \.self) { indexColor in // 書き換え
Button {
selectedColor = indexColor // 書き換え
print("allColors: \(allColors)") // 参考のために表示追加
print("indexColor: \(indexColor)") // 参考のために表示追加
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(indexColor) // 書き換え
}
.buttonStyle(.plain)
}
これで実行すると最初の動作と全く同じにように、押されたボタンの色にタイトルが変わります。
左上のピンクの四角ボタンを押してみた時のprint表示は次のようになりました。
allColors: [pink, red, orange, yellow, green, mint, teal, cyan, blue, indigo, purple, brown, gray]
indexColor: pink
ForEachにはallColorsという13色の配列が渡され、その1つの色がindexColorと定義した変数に代入されて呼ばれます。あとはその色をボタン自身の色foregroundColorとタイトルに使う色SelectedColorに代入しています。
書き換えたコードではわかり易さのためindexをindexColorと書き換えましたが、indexのままでも問題なく動作します。ここは自由な変数名でかまいません。
ForEachの理解が少し進んだ、と思っていただけたら幸いです。
ForEachはいつ呼ばれるのか? Buttonとの関係は?
ちょっとまた横道にそれますが、ForEachを理解する上で知っておくと良いことがあります。Buttonの記述と合わせて見て見ましょう。
まず、ここのアプリのButton記述の構成は以下のようになっています。
Button {
// ボタンを押された時の処理
} label: {
// ボタンを表示する時の表記
}
.buttonStyle(.plain)
ここまでの説明では、この「ボタンを押された時の処理」がForEachから呼ばれて実行されていることを理解しました。
では、その下のlabel:部分の「ボタンを表示する時の表記」はいつ呼ばれているのでしょうか。
じゃあ、またprint文追加して確認してみればいんでしょ?
と思いますが、このlabel領域では直接print文は使えないんです!!(ガーン)
Button {
// ボタンを押された時の処理
print文 // OK
} label: {
// ボタンを表示する時の表記
print文 // NG
}
.buttonStyle(.plain)
では、どうするか?と言うと、デバッグ用のちょっとした技を記述しておきます。
Button {
selectedColor = allColors[index]
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(allColors[index])
.onAppear(){ // ←ここを追加
print("index: \(index)")
}
}
.buttonStyle(.plain)
上のコードのように、label部のRoundRectangleに.onAppearというモディファイヤーを追加しました。その中には処理をかけるので、printを記述します。
onAppearはRoundRectangleが描画される直前に呼ばれます。
つまりButtonのlabel部が呼ばれてRoundRectangleを描画する時に前もって呼ばれます。
注意:onAppearはRoundRectangleが実際に描画される直前に呼ばれるので、ForEach内で呼ばれる時とはタイミングや順番が変わることがあります。
では実行してprint結果を見て見ましょう。
アプリを実行しただけで、まだボタンは押してない状態での表示です。
index: 5
index: 11
index: 8
index: 2
index: 4
index: 10
index: 12
index: 9
index: 6
index: 0
index: 1
index: 7
index: 3
順番は別として、ボタンの数の13回、それぞれのindexで呼ばれているのがわかりますね。
実際には起動時だけでなく、他のViewから戻った時や、表示内容が変わった時などにも呼ばれます。
ForEachの呼ばれるタイミングを簡単にまとめます。
ForEachはその内部のViewを表示する度に、回数分呼ばれます。
また、ボタン等が押された時にも、そのindexだけ呼ばれます。
これを理解していないと、常にForEachが呼ばれてしまうオーバーヘッドの大きいプログラムになってしまうこともありますので、知っておいて頂けると良いかと思います。
それでは、次は後回しにしてきた id: \.self の話に入っていきます。
id: \.self:ForEachを手軽に使うための魔法ワード
ForEachに渡す配列等はIdentifiableまたはHashableのプロトコルに対応していなければなりません。
と、AppleのDocumentationや解説でも良く書いてありますね。
アイデンティファイアブル? ハッシャブル? プロトコル?
ここは私が1回目の挫折しそうになった箇所(笑)ですので、
自分のためにも、読んで頂いている方のためにも、なるべく簡単に解説しておきますね。
それぞれ解説してるとかなり長くなってしまうので、ほんとに簡単に。
Protocol(プロトコル)とは?
IdentifiableもHashableもプロトコルの1つですので、まず先にプロトコルを理解しておきましょう。
深く理解されたい方は以下のページをおすすめします。(英語です)
the swift programming language swift 5.7
超簡単に言いますと、プロトコルとは、
構造体やクラスが持っていなければならない条件を規定するものです。
あくまで例ですが、
プロトコルAに準拠させたい場合は”name: String”という変数(プロパティ)を必ず持ってください。
プロトコルBに準拠させたい場合は”func addObject”という関数(メソッド)を必ず持ってください。
みたいなことを規定しているものです。
こんな単純なことを理解するまでにかなり時間がかかった覚えがあります(笑)
Identifiableプロトコル
ここも超簡単に言うと、Identifiableプロトコルに準拠するということは、
そのクラスや構造体が必ずidという変数(プロパティ)を持ち、その各idは絶対に唯一無二の値であることが保証されている、ということを意味します。
ちょっと横道にそれますが、Playgroundsの別なアプリ「予定表」の中のイベントの部分のコードを見ると、記述例として分かり易いです。
struct EventTask: Identifiable, Hashable {
var id = UUID()
var text: String
var isCompleted = false
var isNew = false
}
構造体EventTaskはIdentifiableプロトコルとHashableプロトコルの両方に対応していて、idのプロパティを持っている例です。UUID()は生成される度に唯一無二の値を作る便利な構造体です。
予定のイベントが、他の別なイベントと混同したら困りますよね。
ForEachは渡される配列等にIdentifiable準拠が求められています。
なぜかと言うと、「これは前回この場所に描画したあれ」と、ForEachが絶対的に判別できる手段を持たないと、表示がおかしくなったり、プログラムが破綻したりする可能性を拭えないからだと理解してます。
HashableプロトコルとForEach
Hashableプロトコルを超簡単に理解するなら、
Identifiableプロトコルより少し簡易なイメージです。唯一無二ではなく、ハッシュ値を算出する関数に代入する値が同じならば、必ず同じハッシュ値が算出されます。逆に言うと全てに違いがあれば、全て違うハッシュ値になります。
ForEachでは渡す配列等がIdentifiableプロトコルに対応している必要があると説明してきましたが、id:にHashableな値を添えてあげても良いことになっています。
つまり、基本は
ForEach( Idenfifiableプロトコルに準拠した配列等 ) {
ですが、
ForEach( Idenfifiableプロトコルに準拠してない配列等, id: Hashableプロトコルに準拠する値 ) {
でも利用可能というわけです。
ここに\.selfという値を入れているので、id: \.self という表記になるのですね。
やっと本来のソースコードに戻ってきました!
id: \.selfの意味と役割
もう一度オリジナルのソースコードのForEachのところを見てみます。
以下のように書かれていますね。
ForEach(allColors.indices, id: \.self) { index in
// Buttonの処理
}
ここまで読んで頂けてれば、単に整数の配列であるallColors.indicesに、idでHashableな値を添えてForEachがそれぞれのオブジェクトを認識できるようにしているということがわかります。
ForEachの定義を見ると、このidにはKeyPathというの渡すことになっています。KeyPathとは、あるプロパティを指し示すアドレスみたいなものです。ある構造体のある要素を指すアドレスは普通1つですね。それを使っているのだと思います。
KeyPathに関して大変わかり易く解説されている方がいらしたので興味のある方はご覧ください。
[Swift] KeyPath の理解
\.selfは対象となる自身の要素(allColors.indicesのどれか)のKeyPathを指しています。
これで実際に機能しているのか確認して見ましょう
id: \.selfは直接見れないので、各hashValueを見てみることにします。
ForEach(allColors.indices, id: \.self) { index in
Button {
selectedColor = allColors[index]
print("index: \(index), hashValue: \(index.hashValue)") // ←追加
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(allColors[index])
}
.buttonStyle(.plain)
}
このコードで起動し、ボタンを左上から順番に押していった結果が次のようになりました。
index: 0, hashValue: -8304542313782559366
index: 1, hashValue: -5248103452417764555
index: 2, hashValue: -5338204073595650304
index: 3, hashValue: -8825484637452873633
index: 4, hashValue: 1169913083888821917
index: 5, hashValue: -7690722083909106485
index: 6, hashValue: -6786457752310337799
index: 7, hashValue: 1868106449629457362
index: 8, hashValue: 2711594031788422043
index: 9, hashValue: 3088670096863485189
index: 10, hashValue: -6142757464921222429
index: 11, hashValue: 622420446107043753
index: 12, hashValue: 3364509115262895935
それぞれ20桁近い数字が入っていました。これなら唯一無二のIDとして使えますね。
ForEachにおけるid: \.selfのまとめ
与える配列等がIdentifiableプロトコルに準拠していれば、次のように記述。
ForEach( 配列等 ) {
しかし一般的な配列(例えば[Int]や[String]など)を使いたい場合は、次のように記述。
ForEach( 配列等, id: \.self ) {
使い方がスッキリしましたね!
最後に
2回に分けてiPadのSwift Playgroundsの「グリッドを使った整理」を解説して見ました。
わずか90行程度のソースコードの中に学ぶべきことが沢山あったと思います。
最後まで読んで頂いて、ありがとうございます。
次回は「グリッドの編集」で、1NavigationView関連をわかり易く解説してみたいと思います。
ご意見、ご指摘等ありましたら、コメントを頂けると大変嬉しいです。
コメント