クラッシュするパターンとしないパターンの紹介。
とりあえず現状のメモ。
クラッシュする例
-
List
の中でForEach
して、.onDelete
やら.onMove
を設定 - 行の中身は、データをバインド渡しした子ビューに任せる。
- 一見問題なく動くように見えるが、最終行を削除するとクラッシュ
import Foundation
import SwiftUI
// お店は商品を複数持っている
struct Shop {
var name: String
var items: [Item]
}
// 商品。とりあえず名前だけ。
struct Item: Identifiable {
var id = UUID()
var name: String
}
// 親ビュー
struct SimpleList: View {
// サンプルデータ。
@State var shop = Shop(
name: "くだもの屋さん",
items: [
Item(name: "りんご"),
Item(name: "ばなな"),
Item(name: "みかん")
]
)
var body: some View {
VStack {
Text(shop.name).font(.title)
HStack(spacing:20) {
Button(action: onAdd){ Image(systemName: "plus") }
Spacer()
EditButton()
}
List {
// お店の商品でForEach
ForEach(shop.items){ item in
// 子ビューにぜんぶ任せる、バインドで!
SimpleSubView(item: $shop.items[id2index(item.id)])
}
// 移動・削除の処理
.onMove(perform: onMove)
.onDelete(perform: onDelete)
}
}
.padding()
}
// IDからリストのindex番号取得
func id2index(_ id: UUID) -> Int {
return shop.items.firstIndex(where: { $0.id == id })!
}
// お店の商品追加、移動、削除
func onAdd() {
let newItem = Item(name: "new item")
shop.items.append(newItem)
}
func onMove(_ iset: IndexSet, _ newOffset: Int) {
shop.items.move(fromOffsets: iset, toOffset: newOffset)
}
func onDelete(_ iset: IndexSet) {
shop.items.remove(atOffsets: iset)
}
}
// 子ビュー
struct SimpleSubView: View {
// 編集したり色々する予定でバインドで受け取っている
@Binding var item: Item
var body: some View {
Text(item.name) // が、今はとりあえず、単純に表示
}
}
動作スクリーンショット
エラーメッセージ
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
(タイムスタンプ) (プロジェクト名)[9600:1174243] Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
(lldb)
インデックスが範囲外…?
クラッシュしない例
ググったり試したりして分かったことは、
- 「
ForEach(リスト, id:_)
てな風に明示的にidを指示せよ」的な記事を見つけたけど、いま現在(2021年1月)id指定しても関係なかった。 - 子ビューに渡さず、直接ForEach内に書けばクラッシュしない
- バインドではなく、値で子ビューに渡せばクラッシュしない
ForEach内に長々とコードを書くのは、後々見づらくて後悔しそうな気がする。
なので、子ビューへは値で渡しつつ、何とかして子ビューから元のリストを更新する方向で考えた。
リストはリストで別途、バインドで渡してみた
// 親ビューのForEachの中
// SimpleSubView(item: $shop.items[id2index(item.id)])
SimpleSubView(item: item, items: $shop.items)
// 親の id2index(UUID)->Int は不要になる。
// 子ビュー
struct SimpleSubView: View {
@State var item: Item // 普通に値で受け取る
@Binding var items: [Item] // 加えてリスト全体をバインドで受け取る
var body: some View {
HStack {
// 値で受け取ったitemで編集。コミット時にバインドされたリストに反映
TextField("name", text: $item.name, onCommit: commit)
}
}
// 値をリストに反映する
func commit() {
// リストから自分のIDを探してインデックス取得。
// 見つからない(たぶん親が削除した)なら、何もせず終了。
guard let index = items.firstIndex(where: { $0.id == item.id })
else { return }
// 編集内容をリスト内の商品に反映。
// indexが存在することは確認済み、out of index とは言わせないぞ
items[index].name = item.name
}
}
消しても大丈夫になった。
- リストをバインドで渡す代わりに、
@EnvironmentObject
に入れる手もある。- そっちに上記の
commit()
相当のメソッド作って、子ビューではitem
丸ごと渡すだけ、にすれば楽そう
- そっちに上記の
- 子ビューで
commit()
を呼ぶタイミングは要検討。-
TextField
のonCommit
はエンターキー押したときとかなので、操作方法によって発生しないケースもあるかも - 編集画面と表示画面が別で、編集終わったら画面切り替わる、とかだったら、
.onDisappear
で確実に反映できるのでは?
-