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

# 概要

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`の判定を追加しました。
[ドキュメント](https://developer.apple.com/documentation/coredata/nsmanagedobject/1506837-isfault)にもある通り、`true`の場合、オブジェクトがメモリ上にないことを示します。

ドキュメントの引用
>`if this property is true, it does not mean that the data is not in memory. `

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

# 補足
[Stack Overflowの記事](https://stackoverflow.com/questions/59123439/swiftui-holding-reference-to-deleted-core-data-object-causing-crash)にもある通り、これがベストな方法なのかがわからないです。

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

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


#  コード

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

<details><summary>ListView.swift</summary><div>

```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()
        }
    }
}
```
</div></details>



## ListRowView
各ToDoのRowです。

<details><summary>ListRowView.swift</summary><div>

```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)
            }
        }
    }
}
```
</div></details>

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

<details><summary>DetailView.swift</summary><div>

```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()
        }
    }
}
```
</div></details>