Core Dataで、各種Viewとデータバインディング

Core DataとNSFetchedResultsControllerを使えば、Core Dataに変更が入るたびにViewにも変更が自動反映されるようになって超便利なのですが、いっつも書き方を忘れるので、各種ビューでの実装方法を記述します。

ちなみに公式のドキュメントは[こちら](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/nsfetchedresultscontroller.html)

以下は、`Movie` という Core DataのEntityが存在すると仮定しての実装方法になります。

# NSFetchedResultsControllerの実装方法例

fetchedResultsController.delegateに設定する `NSFetchedResultsControllerDelegate` やソート条件は適宜環境に合わせて読み替えてください。

```swift
    lazy var fetchedResultsController:NSFetchedResultsController<Movie> = {
        let fetchRequest:NSFetchRequest<Movie> = Movie.fetchRequest()
        fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Movie.createAt, ascending: true)]
        let fetchedResultsController = NSFetchedResultsController<Movie>(fetchRequest: fetchRequest, managedObjectContext:context, sectionNameKeyPath: nil, cacheName: nil)
        fetchedResultsController.delegate = fetchedResultsControllerDelegate
        try! fetchedResultsController.performFetch()
        return fetchedResultsController
    }()
```

# 各種ビューへの反映

## UITableView

### UITableViewDataSource
公式のドキュメントとほぼ同じのコードです。

```swift
    func numberOfSections(in tableView: UITableView) -> Int {
        if let count = fetchedResultsController.sections?.count {
            return count
        }
        return 0
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let sections = fetchedResultsController.sections else {
            fatalError("No sections in fetchedResultsController")
        }
        let sectionInfo = sections[section]
        return sectionInfo.numberOfObjects
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let object = fetchedResultsController.object(at: indexPath)
        let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
        // 以下、cellにobjectのデータを反映させる処理
    }
```

### UITableView with NSFetchedResultsControllerDelegate

アニメーション部分 `with` はお好みで。

```swift
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .delete:
            tableView.deleteRows(at: [indexPath!], with: .automatic)
        case .insert:
            tableView.insertRows(at: [newIndexPath!], with: .automatic)
        case .move:
            tableView.moveRow(at: indexPath!, to: newIndexPath!)
        case .update:
            tableView.reloadRows(at: [indexPath!], with: .automatic)
        @unknown default:
            break
        }
    }
    
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }
```

## UICollectionView

### UICollectionViewDataSource

```swift
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        if let count = fetchedResultsController.sections?.count {
            return count
        }
        return 0
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        guard let sections = fetchedResultsController.sections else {
            fatalError("No sections in fetchedResultsController")
        }
        let sectionInfo = sections[section]
        return sectionInfo.numberOfObjects
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
        let object = fetchedResultsController.object(at: indexPath)
        // 以下、cellにobjectのデータを反映させる処理
    }
```

### UICollectionView with NSFetchedResultsControllerDelegate

UICollectionViewには、UITableViewと違い、`beginUpdates`や`endUpdates`が無く、`controller(_:didChange:at:for:newIndexPath:)`で変更点を蓄えておき、`performBatchUpdates(_:completion:)`で反映を行います。

```swift
    private var blocks:[() -> Void] = []

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .delete:
            blocks.append { [weak self] in
                self?.collectionView.deleteItems(at: [indexPath!])
            }
        case .insert:
            blocks.append  { [weak self] in
                self?.collectionView.insertItems(at: [newIndexPath!])
            }
        case .move:
            blocks.append { [weak self] in
                self?.collectionView.moveItem(at: indexPath!, to: newIndexPath!)
            }
        case .update:
            blocks.append { [weak self] in
                self?.collectionView.reloadItems(at: [indexPath!])
            }
        @unknown default:
            break
        }
    }
    
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        blocks.removeAll()
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        collectionView.performBatchUpdates({
            self.blocks.forEach { (block) in
                block()
            }
        }, completion: nil)
    }
```

## WKInterfaceTable for watchOS (おまけ)

WKInterfaceTableには、`performBatchUpdates`や`begin/endUpdates`などの仕組みがないので、`controllerDidChangeContent(_:)`でまとめて更新してしまうのが楽です。

```swift
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    }
    
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        guard let fetchedObjects = controller.fetchedObjects else { return }
        table.setNumberOfRows(fetchedObjects.count, withRowType: identifier)
        let rowCount = table.numberOfRows
        for i in 0..<rowCount {
            let object:Movie = fetchedObjects[i] as! Movie
            let row = table.rowController(at: i)
            // 以下、rowにobjectのデータを反映させる処理
        }
    }
```

つかおう! Core Data!