Core DataでCollectionViewを更新する


## そもそもの動機
- dataSourceの変更時、controllerDidChangeContentでreloadDataを使っていた
  - 一気に更新できるが、新規追加されたコンテンツの追加の表現がこなれていない
  - 新規追加分や削除されたもののみがiOSらしくシュッとアニメーションしてほしい

## 前提

- Core DataをUICollectionViewに表示する際の話
    - NSFetchedResultsControllerを使ってdataSourceの変更を監視し自動でUICollectionViewを更新する
    - reloadDataを使わない
- sectionは単一
    - この記事ではsection=0しか扱わないが、sectionがある場合は後述するNSFetchedResultsControllerDelegateのメソッドが追加で1つ必要

## 要約

- dataSourceの変更検知
    - NSFetchedResultsControllerDelegate
        - controllerDidChangeContentはdataSourceの変更後にviewの更新をまとめて行う
            - reloadDataは使わない。後述のperformBatchUpdatesを使う
        - [controller(_:didChange: at:for:newIndexPath:)](https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate/1622296-controller)
            - 変更される前に対応するindexPathを含めて通知される
- UITableViewではbeginUpdate/endUpdateでまとめてviewを更新していた
    - CollectionViewにbeginUpdate/endUpdateはない
        - CollectionViewではperformBatchUpdatesを使う
- 実装方法
    - [controller(_:didChange: at:for:newIndexPath:)](https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate/1622296-controller)
        - 変更されるindexPathとnewIndexPathを変更方法と共に `Array` に追記
    - [controllerDidChangeContent(_:)](https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate/1622290-controllerdidchangecontent)
        - performBatchUpdatesを呼び出し`Array`を取り出して処理

## 実装

```swift

struct CollectionViewContentChange {
    let type: NSFetchedResultsChangeType
    let indexPath: IndexPath?
    let newIndexPath: IndexPath?
}

final class ContentsViewController: UIViewController {

    fileprivate weak var collectionView: UICollectionView!
    // HogeというNSManagedEntityがあるとして
    fileprivate lazy var frc: NSFetchedResultsController<Hoge> = {
        ...省略...
    }()

    override func viewDidLoad() {
       frc.delegate = self
       ...省略...
    }
}

extension ContentsViewController: NSFetchedResultsControllerDelegate {
    // 変更されるindexPathとnewIndexPathを変更方法と共に Array に追記
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                    didChange anObject: Any,
                    at indexPath: IndexPath?,
                    for type: NSFetchedResultsChangeType,
                    newIndexPath: IndexPath?) {

        let change = CollectionViewContentChange(type: type, indexPath: indexPath, newIndexPath: newIndexPath)
        contentsChanges.append(change)
    }

    // performBatchUpdatesを呼び出しArrayを取り出してviewを更新する処理
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {

        // 重要なのは performBatchUpdates
        collectionView?.performBatchUpdates({
            for contentChange in self.contentsChanges {
                // 変更を取り出して処理させる
                self.performCollectionViewChange(contentChange)
            }
        }, completion: { _ in
            // 終了したらカラにしておく
            self.contentsChanges = [CollectionViewContentChange]()
        })
    }
}

private extension ContentsViewController
    // 実際にviewに反映する処理
    func performCollectionViewChange(_ contentChange: CollectionViewContentChange) {

        switch contentChange.type {
        case .insert:
            collectionView.insertItems(at: [contentChange.newIndexPath!])
        case .update:
            collectionView.reloadItems(at: [contentChange.indexPath!])
        case .move:
            collectionView?.moveItem(at: contentChange.indexPath!, to: contentChange.newIndexPath!)
        case .delete:
            collectionView?.deleteItems(at: [contentChange.indexPath!])
        }
    }
}
```

## 参考

このコードは以下を参考にしている

tempire/collectionview-beginend-updates.swift
https://gist.github.com/tempire/debbabb2cccafa90320e

もしsectionが複数ある場合

iwasrobbed/gist:5528897
https://gist.github.com/iwasrobbed/5528897

上記例はOperationを使っているが、frcがメインスレッドで生成されていれば、変更検知はメインスレッドで動作するのではないかと思うので必要性を感じない。