3
2

More than 3 years have passed since last update.

SwiftUI × CoreData データ削除時のクラッシュについて

Posted at

概要

SwiftUI と CoreData の練習のためにToDoアプリを作成していたところ、
データの削除時にクラッシュする事象がありましたのでその解決方法を記載します。

環境

macOS : Catalina(10.15.3)
Xcode : Version 11.4
Swift : 5.2

原因

Viewの構成としては、
ListView:ToDoの一覧をListで表示する
ListRowView:ToDoの一覧の各ToDoのデータを表示する。DetailViewで更新があった場合、このViewにも反映させる
DetailView:ToDoの詳細を表示する。ToDoの一覧からナビゲーションリンクで画面遷移する。ToDoの更新を行う
となっています。

ListViewでfetchしたCoreDataのオブジェクトをListRowViewとDetailViewに@EnvironmentObjectで参照渡ししています。
これは、DetailViewでオブジェクトの更新をし、さらに更新内容をListRowViewに反映させるためです。

Listをスワイプして削除した際、CoreDataのオブジェクトは削除されるのですが、ListRowViewとDetailViewがそれぞれ削除されたオブジェクトを参照しておりクラッシュしました。
エラーは以下です。
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

解決方法

CoreDataのオブジェクトのisFaultの判定を追加しました。
ドキュメントにもある通り、trueの場合、オブジェクトがメモリ上にないことを示します。

ドキュメントの引用

if this property is true, it does not mean that the data is not in memory.

isFaultがfalseの場合のみ、CoreDataのオブジェクトを参照して、Viewを表示するようにします。

補足

Stack Overflowの記事にもある通り、これがベストな方法なのかがわからないです。

また、Stringの項目ではなく、Dateのクラッシュするのが、わからないところです。。。

もし、何かご存知の方がいらっしゃいましたら、コメントよろしくお願いします。

コード

ListView

ToDoの一覧です。
CoreDataから取得して、Listで表示します。
各ToDoの表示はListRowViewで行います。

ListView.swift
ListView.swift

struct ListView: View {
    @Environment(\.managedObjectContext) var context

    @FetchRequest(sortDescriptors: [], animation: .default)
    private var toDos: FetchedResults<ToDoData>

    var body: some View {
        NavigationView {
            List {
                ForEach(toDos) { toDo in
                    NavigationLink(destination: DetailView().environmentObject(toDo)) {
                        ListRowView().environmentObject(toDo)
                    }
                }
                .onDelete { indexSet in
                    self.deleteToDoData(indexSet: indexSet)
                }
            }
            .navigationBarTitle("ToDoList")
    }
    private func deleteToDoData(indexSet: IndexSet) {
        for index in indexSet {
            context.delete(toDos[index])
        }
        do {
            try context.save()
        } catch {
            fatalError()
        }
    }
}

ListRowView

各ToDoのRowです。

ListRowView.swift
ListRowView.swift
struct ToDoListRow: View {
    @EnvironmentObject private var toDo: ToDoData

    private var deadlineColor: Color {
        if toDo.deadline < Date() {
            return Color.red
        } else {
            return Color.black
        }
    }
    var body: some View {
        HStack {
            if toDo.isFault {  // ←ここがポイント!!
                Text("")
            } else {
                VStack(alignment: .leading) {
                    Text(toDo.title)
                        .font(.title)
                    HStack {
                        Text("Deadline : ")
                        Text(dateFormatter.string(from: toDo.deadline))
                            .foregroundColor(deadlineColor)
                    }
                    .padding(.bottom)
                    HStack {
                        if toDo.completedDate != nil {
                            Text("Completed")
                        }
                    }
                }
                .padding(.leading)
            }
        }
    }
}

DetailView

各ToDoの詳細を表示するViewです。

DetailView.swift
DetailView.swift
struct ToDoDetailView: View {
    @Environment(\.managedObjectContext) var context

    @EnvironmentObject private var toDo: ToDoData

    var body: some View {
        List {
            if toDo.isFault {  // ←ここがポイント!!
                Text("")
            } else {
                Section(header: Text("Title")) {
                    Text(toDo.title)
                        .font(.largeTitle)
                }
                Section(header: Text("Deadline")) {
                    Text(dateFormatter.string(from: toDo.deadline))  // ← isFaultの判定がないとここでクラッシュする
                        .font(.largeTitle)
                }
                if toDo.completedDate != nil {
                    Section(header: Text("Completed")) {
                        Text(dateFormatter.string(from: toDo.completedDate!))
                            .font(.largeTitle)
                    }
                } else {
                    Button(action: {
                        self.completeToDo()
                    }) {
                        Text("完了にする")
                    }
                }
            }
        }
        .listStyle(GroupedListStyle())
        .navigationBarTitle("ToDoDetail")
    }
    private func completeToDo() {
        toDo.completedDate = Date()

        do {
            try context.save()
        } catch {
            fatalError()
        }
    }
}

3
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
3
2