MVVMを壊す悪魔、@FetchRequestを駆除する(Core Data)

# 注意（2022/6/19追記）
まずViewModelの機能を併せ持った`View`があるのに、やたらとMVVMにこだわるのはお勧めできません。
https://qiita.com/karamage/items/8a9c76caff187d3eb838 を参照してください。

# @FetchRequestが駆除されなければならない理由
（釣りタイトルですいません。）
SwiftUIとCoreDataを組み合わせたアプリの例では、ほとんど全てと言っていいほど@FetchRequestが使われています。しかしこのプロパティラッパーは現時点でViewの内部でしか使えないため、必然的にViewがUIに表示するデータを持ってしまい、MVVMが成り立たなくなります。よってこの記事では、このプロパティラッパーを使わずにViewModel側からデータをフェッチし、なおかつViewを自動で更新する方法を紹介します。
（注意：初学者なので間違いがあるかもしれません。その場合コメントで指摘をお願いします。）

**2021年7月23日 追記**
ここまでMVVMにこだわる意味をあまり感じなくなってきました。Appleさんも、完全にMVVMでアプリを構築できるフレームワークにするつもりはないのでしょう。

# 方法
## 1. NSFetchedResultsControllerDelegate
https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate
公式ドキュメントには「フェッチ結果が変化したときに、関連するfetched result controllerによって呼び出されるメソッドを記述するデリゲート」とあります。簡単に言うと、データが変わったときにメソッドが呼ばれるよ、ということです。下記のコードは、DiaryというEntityをフェッチし、Viewに表示するViewModelです。
(DiaryDataModelというのはcontextを取得するためのクラスで、MVVMのModelにあたります。)

```Swift:DiaryListViewModel.Swift
import Foundation
import CoreData

final class DiaryListViewModel: NSObject, NSFetchedResultsControllerDelegate, ObservableObject {
    private let fetchController: NSFetchedResultsController<Diary>
    
    var diaries: [Diary] {
        return fetchController.fetchedObjects ?? []
    }
    
    override init() {
        fetchController = {
            let fetchRequest = NSFetchRequest<Diary>(entityName: "Diary")
            fetchRequest.sortDescriptors = [.init(keyPath: \Diary.createdDate, ascending: true)]
            
            return NSFetchedResultsController<Diary>(
                fetchRequest: fetchRequest,
                managedObjectContext: DiaryDataModel.context,
                sectionNameKeyPath: nil,
                cacheName: nil
            )
        }()
        super.init()
        fetchController.delegate = self
        try? fetchController.performFetch()
    }
    
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        print("❗️controllerWillChangeContent is called.")
        objectWillChange.send() //更新をSwiftUIへ知らせる
    }
    
    ......
    
}

```

`fetchController`の初期化の書き方はわかりにくかもしれませんが、クロージャに最後()をつけて実行し、戻り値をセットしているだけです。

```try? fetchController.performFetch()```で実際にフェッチさせて、`fetchController`にデータを読ませます。どうやらデータベースの変更があっても再度フェッチする必要はないようです。

`NSFetchedResultsControllerDelegate`に定義されている```controllerWillChangeContent```メソッドでは、変更をSwiftUIに知らせるために```objectWillChange.send()```を呼びます。

`diaries`はget-onlyの計算型プロパティで、実際にView側からアクセスするところです。`objectWillChange.send()`が呼ばれるとSwiftUIによって再度読み込まれ、Viewに反映されます。

Viewの方はこんな感じです↓

```Swift:DiaryListView.Swift
import SwiftUI

struct SidebarView: View {
    @StateObject private var viewModel = DiaryListViewModel()
    //ViewがViewModel以外にプロパティを持っていない...美しぃ...？
        
    var body: some View {
        List {
            if !viewModel.diaries.isEmpty {
                ForEach(viewModel.diaries) { diary in
                    Label(diary.name, systemImage: "book")
                }
            } else {
                Text("ページがありません。")
            }
        }
    }
}
```

`NSFetchedResultsControllerDelegate`の使い方に関しては[この記事](https://augmentedcode.io/2020/01/19/using-coredata-with-swiftui/)をかなり参考にさせていただきました。

## 2.NotificationCenter & Combine(失敗)
`NSFetchedResultsControllerDelegate`を使う方法とほぼ同じですが、肝となる更新の方法は異なり、`NSManagedObjectContextObjectsDidChange`という~~クソ長い~~名前のNotificationを使います。

```Swift:DiaryListViewModel.Swift
import Foundation
import CoreData
import Combine

final class DiaryListViewModel: ObservableObject {
    private var cancellables: Set<AnyCancellable> = []
    private let fetchController: NSFetchedResultsController<Diary>
    
    var diaries: [Diary] {
        return fetchController.fetchedObjects ?? []
    }
    
    init() {
        fetchController = {
            let fetchRequest = NSFetchRequest<Diary>(entityName: "Diary")
            fetchRequest.sortDescriptors = [.init(keyPath: \Diary.createdDate, ascending: true)]
            return .init(fetchRequest: fetchRequest, managedObjectContext: DiaryDataModel.context, sectionNameKeyPath: nil, cacheName: nil)
        }()
        try? fetchController.performFetch()
        
        NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange)
            .sink { _ in
                print("objecgt was changed.")
                self.objectWillChange.send()
            }
            .store(in: &cancellables)
    }
    
    ......
    
}

```

この場合、`sink`のクロージャは呼ばれ、`diaries`プロパティも再読み込みされていることは確認できたのですが、なぜかUIは更新されませんでした。クロージャ内で`objectWillChange.send()`を呼ぶことがいけないのでしょうか......(Combine使ったらカッコいいと思ったのに、残念)。問題点がわかった方、コメントで教えていただけると助かります🙇🏻‍♂️


# あとがき
@AppStorage、@SceneStorageとかも「駆除」できそうですね。というか、ViewModelでも機能するようなプロパティラッパーを作るのもできそうです。作ったらまた記事にするかもしれません。

# その他
## 参考記事
[Using CoreData with SwiftUI](https://augmentedcode.io/2020/01/19/using-coredata-with-swiftui/) : NSFetchedResultsControllerDelegateの使用法など
[Core Data Notifications With Swift](https://medium.com/@marcosantadev/core-data-notifications-with-swift-acc8232a674e) : Core Dataの様々なNotification

## 環境
OS : macOS Big Sur 11.1
Xcode: 12.4
Swift : 5.3.2