SwiftDataでデータを保存できるようになったので、次は保存した記録を画面に一覧表示する番です。飲酒量管理アプリの「記録一覧」画面を作る中で、ListとForEachまわりでつまずいたポイントをまとめます。
まずは一覧表示から
SwiftUIで一覧画面を作る基本形はこうなります。
List(records) { record in
Text(record.date.formatted())
}
配列を渡すだけでスクロール可能な一覧ができる手軽さには驚きましたが、実際に「削除」「並び替え」まで実装しようとすると、いくつも壁にぶつかりました。
1. ListとForEachの使い分けが分からなかった
最初はListだけで全部完結すると思っていたのですが、削除や並び替えのスワイプ操作を細かく制御しようとすると、Listの中にForEachを組み合わせる書き方が必要になることが分かりました。
List {
ForEach(records) { record in
Text(record.date.formatted())
}
.onDelete(perform: deleteRecords)
}
List単体では「表示の枠」しか提供してくれず、削除・並び替えのアクションを紐づけるにはForEachの.onDeleteや.onMoveが必要、という役割分担を理解するまでに時間がかかりました。
2. .onDeleteを付けたのにスワイプ削除ができない
.onDelete(perform: deleteRecords)を書いたのに、実機で左スワイプしても削除ボタンが出てこない、という現象に遭遇しました。
原因は.onDeleteをListではなくForEachに付けていなかったこと(Listに直接付けてしまっていた)でした。.onDeleteはあくまでForEachの修飾子として使う必要があり、順番を間違えると静かに効かなくなる、という点に気づくまで少し時間がかかりました。
3. 削除処理の中でインデックスの扱いを間違えた
deleteRecords関数の中身は、最初こう書いていました。
func deleteRecords(at offsets: IndexSet) {
for index in offsets {
modelContext.delete(records[index])
}
}
一見正しそうですが、複数件を同時に削除しようとすると、1件目を削除した瞬間に配列のインデックスがずれてしまい、意図しないデータまで削除してしまう不具合が起きました。
IndexSetを使う場合は、配列から直接インデックスでアクセスするのではなく、offsets.map { records[$0] }のように一度対象を確定させてから削除する形に書き換えて解決しました。
4. 並び替え(.onMove)を付けたのに順序が保存されない
ドラッグで並び替えられるようにしたくて.onMoveを追加したのですが、アプリを再起動すると並び替えた順序が元に戻ってしまう現象がありました。
.onMove(perform: moveRecords)
.onMoveは「画面上の見た目の順番」を変えてくれるだけで、SwiftDataに保存されているデータの並び順まで自動的に更新してくれるわけではない、ということに気づいていませんでした。各レコードに「並び順」を表すプロパティを持たせて、moveRecordsの中でその値を更新し直す必要がありました。
5. 編集モードのオン/オフでレイアウトが崩れた
削除・並び替え用の「編集」ボタンをEditButton()で用意したのですが、編集モードに入ると一覧のレイアウトが微妙に崩れることがありました。
原因は、一覧の各行に自分で独自のスワイプアクション(.swipeActions)を追加していたことで、標準の編集モードのUIと干渉していたことでした。編集モードを使う画面では、独自のスワイプアクションを併用しない、もしくは編集モード中は非表示にする、という整理が必要だと分かりました。
まとめ
Listは表示の枠、削除・並び替えのアクションはForEachの.onDelete/.onMoveに付ける.onDeleteはListではなくForEachに付ける- 複数削除時のインデックスのズレに注意し、対象を先に確定させてから削除する
.onMoveは見た目の順序のみ変更する。永続化するには並び順プロパティを自分で更新する- 独自のスワイプアクションと標準の編集モードは干渉することがある
一覧表示は「動くものを作る」までは早いのですが、削除・並び替えまで丁寧に作り込もうとすると、意外と細かい仕様理解が必要でした。同じようにSwiftUIでリスト画面を作ろうとしている方の参考になれば幸いです。次は、TestFlightで実際に身内へ配布するまでにやったことをまとめます。
本記事は生成AI(Claude)を活用して執筆・編集しています。内容は実体験に基づき、執筆者本人が確認・監修しています。


コメント