概要
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
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
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
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()
}
}
}