業界トップクラスの求人数を誇る転職エージェントPR

リクルートグループのコネクションを活かした非公開求人も充実、他にはない好条件の求人と出会える

0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SwiftUIメモ: 親ビューでリストを削除すると、子ビューが index out of range で落ちる

Last updated at Posted at 2021-01-21

クラッシュするパターンとしないパターンの紹介。
とりあえず現状のメモ。

クラッシュする例

  • 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)    // が、今はとりあえず、単純に表示
    }
}

動作スクリーンショット

ss2.png
ss3.png
ss4.png
ss5.png

エラーメッセージ

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()を呼ぶタイミングは要検討。
    • TextFieldonCommitはエンターキー押したときとかなので、操作方法によって発生しないケースもあるかも
    • 編集画面と表示画面が別で、編集終わったら画面切り替わる、とかだったら、.onDisappearで確実に反映できるのでは?
0
2
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

Qiita Conference 2025 will be held!: 4/23(wed) - 4/25(Fri)

Qiita Conference is the largest tech conference in Qiita!

Keynote Speaker

ymrl、Masanobu Naruse, Takeshi Kano, Junichi Ito, uhyo, Hiroshi Tokumaru, MinoDriven, Minorun, Hiroyuki Sakuraba, tenntenn, drken, konifar

View event details
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?