【SwiftUI, CoreData】データ登録と動的絞り込みの両方で再描画させる

# はじめに
自分が初心者なので初心者向けです。無理くり解決したもののベストプラクティスではないだろうと思っています。良い方法があればコメントなどお願いいたします。

# やりたかったこと、困ったこと
データの登録（CoreData更新）と絞り込み（predicate変更）のそれぞれで再描画させるのは簡単だったのですが、両方で再描画させるには工夫が必要でした。

両方に対応できた結果のGIFはこちら↓
![gif_memo_label.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734189/deb750f1-1b90-ef85-bdad-67392b83c370.gif)
「登録」を押したタイミングで表示が増え、「ラベルで絞り込み」も機能しています
（ContentViewの全コードは最下部に掲載）

これの何が難しかったかですが、下記の動的絞り込みを実装すると、データ追加のタイミングで再描画されなくなってしまいます
* 動的な絞り込みの例
```swift
memos.nsPredicate = NSPredicate(format: "label contains %@", "a")
```
* データ登録の例
```swift
let newMemo = Memo(context: viewContext)
newMemo.text = "Memo1"
newMemo.label = "aaa"
newMemo.created_at = Date()
do {
    try viewContext.save()
} //・・・以下略・・・
```

# 解決策と考え方
色んなやり方があると思いますが、動的絞り込み用の`@State`に連動させる形を意識しました。大ざっぱにこんな考え方です。

* 動的な絞り込みのやり方を変える
1. 子ビュー`showDataView`の`@FetchRequest`で絞り込み条件を指定
2. 条件は変数にしたいが、引数や同列で宣言したStateは使えない。なので **グローバル変数を使う** 
3. **グローバル変数を`@State`と連動させる**

上記1,2部分のコード抜粋がこちら。グローバル変数`labelFilterGlobal`を`@FetchRequest`の`predicate`に使っています

```swift
var labelFilterGlobal:String = ""
struct showDataView: View{
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Memo.created_at, ascending: true)],
        predicate: labelFilterGlobal == "" ? nil : NSPredicate(format: "label contains %@", labelFilterGlobal),
        animation: .default
    ) private var memos: FetchedResults<Memo>
```

上記3については、さらに細かくいうと下記のような考え方です
3-1. 親ビュー`ContentView`で絞り込み条件の`@State`である`labelFilterState`を宣言。
3-2. `@State`が変更された時の再描画時にグローバル変数`labelFilterGlobal`に代入して連動させる
これで、「親ビューの`@state変更`->グローバル変数更新->子ビューの`@FetchRequest`で絞り込み」の順に動作させられます
```swift
struct ContentView: View {
    @State var labelFilterState:String = ""
    var body: some View {
        get{
            labelFilterGlobal = labelFilterState
            return(
                showDataView()
            )
        //・・・以下略・・・
```

# ContentView全体のコード
最後に冒頭のGIF動画を作った時に動かした全体のコードを掲載します。
（Preview部分はコメントアウトしてシミュレーターをビルドしました）
```swift
import SwiftUI
var labelFilterGlobal:String = ""
struct ContentView: View {
    @State var labelFilterState:String = ""
    var body: some View {
        get{
            labelFilterGlobal = labelFilterState
            return(
                VStack(spacing: 15){
                    Spacer().frame(height:30)
                    addDataView()
                    Divider()
                    showDataView()
                    TextField("ラベルで絞り込み", text: $labelFilterState)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .frame(width: 150)
                    Spacer()
                }
            )
        }
    }
}
struct addDataView: View{
    @State var textInputState:String = ""
    @State var labelInputState:String = ""
    @Environment(\.managedObjectContext) private var viewContext
    var body: some View {
        VStack{
            Text("メモとラベルを登録").font(.headline)
            TextField("メモを入力", text: $textInputState)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(width: 150)
            TextField("ラベルを入力", text: $labelInputState)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(width: 150)
            Button("登録"){ addMemo() }
        }
    }
    func addMemo() {
        withAnimation {
            let newMemo = Memo(context: viewContext)
            newMemo.text = textInputState
            newMemo.label = labelInputState
            newMemo.created_at = Date()
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}
struct showDataView: View{
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Memo.created_at, ascending: true)],
        predicate: labelFilterGlobal == "" ? nil : NSPredicate(format: "label contains %@", labelFilterGlobal),
        animation: .default
    ) private var memos: FetchedResults<Memo>
    var body: some View {
        VStack{
            Text("登録済みのメモとラベル").font(.headline)
            ForEach(memos) { memo in
                HStack(spacing:0){
                    Text(memo.text ?? "")
                    Spacer().frame(width:20)
                    Text("#")
                    Text(memo.label ?? "")
                }
            }
        }
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734189/3da01aa1-5d3f-049f-f73c-bc1a67e12ad0.png)
おわり。